在本次演讲中,我们将以一个真实的应用程序为例,学习如何仅使用核心Java来构建和设计遵循Clojure功能原理的Java应用程序,而无需使用任何库,lambda,流或怪异的语法。 我们将看到这些功能原理可以带来什么好处。
先预告片:
Here are the slides in keynote format, ppt format (untested!) and SlideShare (somehow broken!).
和视频:
欢迎反馈和问题!
Transcript
让我们开始一个测验。 谁能告诉我这是什么?
这就是Clojure的外观,就像您第一次看到它一样。
我想指出这张图片中的3个重要事项。
首先,这很丑陋,有人会说这甚至令人作呕。
第二,很多事情根本没有道理。 例如,为什么要在嘴里放个虫子?
还是那到底是什么,它在做什么?
但最后,也是最重要的是,它是外星人。 当我们查看该图时,就是从人类的角度,从Java的角度来看它。
所以也许我们不公平,并且有足够的时间,
借助hypotoad,那张照片可能看起来像这样:
因为有了理解,观点就会发生变化,这将使我们做出更公正的判断。
我知道您在想什么,Wonderwoman从技术上讲不是外星人,如果我们谈论的是Clojure,那么图片就不见了
一些括号。
但是,如果我们花一些时间研究这些外星人,也许我们会发现一些超能力,可以在我们作为Java开发人员的日常工作中使用。
Clojure
那么,Clojure是什么? Clojure是用于JVM的功能,托管,动态和强类型的lisp。
因此,在本次会议中,我们将以一个实际应用程序为例,了解Clojure如何影响我们的构建方式,我们设计此Java应用程序的方式以及Clojure如何感染我们的Java代码。
为了提供更多背景信息,当我参与这个应用程序时,
我已经有12年的Java开发人员经验,有3年的Clojure开发经验。因此,当我参与其中时,在我的旅程中,我的转型,全神贯注地进入了Wikus Stage 2。
我们将要讨论的应用程序是您在某些博彩行业中发现的这种奖励系统之一。
“如果您将一些钱存入您的帐户,我们将免费为您提供两倍的金额”或“如果您现在加入我们,我们将为您提供1000欧元的免费现金!”。
现在没有人会给您任何免费现金,因此,如果您阅读了《条款和条件》,有资格取回该“免费”现金,要能够获得该现金并将其放入口袋,则必须先下注或下注 ,在系统中执行多次,或执行多项活动。
另外,在我们的特殊情况下,我不确定这是否很常见,客户在有限的时间内下注所有这些赌注。
因此,消除所有的市场营销毛病,我们的应用程序要做的就是这样:
如果他们已经注册了奖金,并且在一定时间内进行了很多次下注,则向客户帐户中添加一些金额。
因此,从实现的角度来看,系统必须做的是知道哪些可用奖金,哪些客户已经注册了其中一项奖金,然后跟踪这些客户的有趣活动,在我们的情况下将是 下注并存款。
Functional vs OO
因此,让我们从第一个区别开始:功能与面向对象。
在通常与FP相关联的许多原理,概念和技术中,我只想关注一个以我个人的经验对我设计应用程序的方式产生最大影响的方面。 我认为这特别重要,因为它易于翻译,可以在大多数语言中使用。
Pure Functions
这是纯函数的概念。
纯函数是一段代码,对于给定的输入,它们始终总是会返回相同的结果。 总是。 因此,相同的输入,相同的输出。
为什么纯函数如此重要? 纯函数有很多好处,但是关键之一是纯函数更易于推论,因为纯函数中的所有代码都取决于输入参数。 因此,您需要保持头脑中的了解纯函数的上下文很小。
纯功能就像物理定律,
因为您知道纯函数将如何工作,并且可以依靠它每次相同地工作。
而且,由于纯函数更易于理解,因此也意味着它们易于更改。
变化是我们开发人员谋生的手段。
程序员一直处于维护模式。 实用程序员
我们很少编写新代码。 大多数时候,我们只是在现有系统上进行更改。 即使您在15分钟前创建了项目,但如果更改现有代码,您就已经在业务中。
Side effects
当谈论纯函数时,我们必须谈论其邪恶的孪生副作用。
副作用是使您的代码难以理解的原因,因为突然间,要理解一段代码,仅仅看那段代码是不够的。 现在,您还需要了解其所有依赖性,正在使用的所有库,数据库中的所有可能状态,网络的所有可能状态以及多个线程可以在该处执行的所有可能操作。 同时。
您需要牢记的上下文非常重要。
一旦有副作用,
您不确定进行更改时会发生什么。
好吧,这有点副作用。 这一直在我们身上发生。 您在应用程序的一侧进行了很小的更改,您认为这是无害的更改,然后突然在另一侧突然中断了一个完全不相关的功能,而您根本没有注意到。
因此,对我来说,函数式编程的一个关键见解就是副作用是敌人。
因此,函数式编程是关于消除和控制副作用。
基本上有两种副作用:
- 那些会改变应用程序状态的副作用然后我们有IO副作用。 在IO副作用中,我想将输入副作用(有时称为共效应)与输出副作用(简称为输出效应)区分开。
State
因此,让我们从谈论如何对抗国家开始。
对于奖金系统,我们的应用程序必须跟踪每个客户的状态及其在奖金活动中的进度。
为此,您可以对应用程序保留一个映射进行映像,以客户端ID为键,某种ClientBonus对象为值。 该ClientBonus对象本身可能具有Client对象和Bonus对象。 此外,它还必须跟踪所进行的存款,因此它可能具有一个DepositList,其中将包含一堆Deposit对象,并且每个deposit对象可能包含更多对象。 以类似的方式,它需要跟踪客户所做的赌注。
因此,这些将成为我们的对象图,并且每个客户端将拥有这些图之一。
现在,当涉及到状态管理时,我通常将使每个对象负责其自己的状态更改。 因此,Map将拥有自己的少量设备,少量的代码来管理其内部状态,并确保如果多个线程尝试操纵或读取该Map,它们将看到一致的状态。
以类似的方式,ClientBonus对象还将负责操纵自己的状态内部状态并提供其一致的视图,并且每个对象的状态都相同。
现在,从复杂性的角度来看,每个对象拥有自己的机器来控制其状态意味着什么? 每当更改代码时,这些小机器中的每一个都是可能要考虑的副作用。 您不仅必须考虑必须编写的业务逻辑,还必须考虑由于并发访问对象而导致的任何时序问题。
因此,代码在同一位置混合了业务规则和并发规则。
因此,Clojure告诉我们的是,为简化起见,您要做的是将应用程序逻辑和业务逻辑与任何状态管理分开,因此您无需同时考虑这两者。
那么我们该怎么做呢? 首先,通过使所有内容不可变,所有内容都是一个值,即使是包含所有ClientBonus对象的地图。 既然一切都是不可变的,那么在编写应用程序逻辑时,您无需考虑时序,也不必关心其他线程同时在做什么,因为它们都无法更改为对象图。 因此,这使您解放了思想,这使编写应用程序逻辑变得更加简单。
对于状态管理部分,Clojure附带了一个称为Atom的构造或类,该构造或类与Java AtomicReference基本相同,但功能更多。
原子=〜j.u.c.a.AtomicReference
让我们看看一个原子是如何工作的。
原子拥有对整个不可变值和整个状态的引用,您作为开发人员的工作是编写一个函数,该函数将当前状态作为参数,并产生新状态,原子机制将完成 确保以原子方式完成从一种状态到另一种状态的转换。
为了更好地理解它,让我们看看如果两个线程尝试同时修改该状态会发生什么。
两个线程都将获得初始状态,并将开始计算下一个状态。 可以说线程1在线程2之前完成。 此时,线程1尝试将原子的状态更改为新的绿色值。 为此,它告诉原子执行原子比较和交换操作。 由于用于计算绿色状态的值仍为白色值,因此原子将其状态更改为绿色值。
现在线程2完成,当它尝试更改原子的状态时,比较和交换操作失败,因为原子不再指向白色状态。 因此线程2必须重新启动,但这一次是绿色值。
所有有关重试和检测冲突的机制都是由原子提供的,因此,作为开发人员,您只需要编写一个纯函数即可。
现在,要确保我们都在同一页面上,我想指出两点。
第一个是原子在此期间唯一有效的值是白色,绿色和红色。 没有人看到过蓝色的价值。
另一件事是,例如,如果有另一个线程(线程3),在时间0读取原子的当前状态,并且随着时间从t0到t1和t2的移动,它保持对该原子的引用一段时间, 线程3仍将看到初始状态,即白色状态。 因为该值是不可变的,所以没有人可以触摸它,这意味着线程3可能正在使用过时的值。
现在您可能想知道,哇,如果我每次想更改任何内容时都必须创建一个全新的图形,难道所有的不变性都不会变得非常缓慢,非常昂贵吗? 实际上,它实际上要慢一些,但没有您期望的那么快。
假设您处于这种状态,
并计算新状态,您需要在该位置更改该下注对象中的某些字段。 当然,您不能进行任何更改,因此您可以创建一个新的下注对象。 由于该赌注属于BetList,并且所有内容都是不可变的,这也意味着您必须创建一个新的BetList,这也意味着您必须创建一个新的ClientBonus和哈希表上的一个新存储桶。
这四个方面是绿色和白色状态之间的区别。 因此,要建立绿色状态,您只需要创建4个新对象,就可以重用所有其他对象,并且可以这样做,因为这些对象都是不可变的,并且我们知道共享不可变的对象是安全的。 这种技术称为结构共享。
现在,这仍然比在Bet对象中更改一个字段还要慢,但是成本仍然非常便宜,特别是如果将它与这种方法的好处进行比较的话。
这个线程安全吗? 每天每个Java开发人员。
你有没有问过自己这个问题? 对于不变性和原子性,您仍然会问这个问题,但是回答这个问题的规则要简单得多,因为它们不涉及Java内存模型和“先于先后”的语义。
第一条规则是原子内的状态始终是一致的,因为一切都是不可变的,因此不可能看到半熟状态。 这已经消除了代码中的许多复杂性。
第二条规则是计算新状态的函数必须是纯函数,因为它可能运行多次。
正如我们在Thread-3的示例中所看到的那样,您在此纯函数之外做出的任何决定都可以使用陈旧的数据来完成,也可以遵循竞争条件。
根据第三条规则,很明显,如果您的代码必须查看两个原子来做出某些决定,则该决定不是原子的。
那么这一切如何影响我们的Java代码?
首先,显而易见的是,所有域类都是不可变的,因此所有字段(包括任何映射或列表)都是不可变的。
public class ClientBonus {
private final Client client;
private final Bonus bonus;
private final DepositList deposits;
…
现在,对于状态管理部分,我们实际上并未使用AtomicReference来存储带有所有ClientBonus的整个地图。
在我们的案例中,就像一个客户所做的那样,不会影响另一个客户的奖金的结果,我们的应用程序逻辑只需要ClientBonus保持一致,就不需要当前所有ClientBonuse的一致视图。
因此,我们要做的是实际使用ConcurrentHashMap来保存状态,然后每个值都将拥有自己的少量机制来延长时间。
该机制由ConcurrentMap的计算方法家族提供,这些方法基本上提供与Clojure原子相同的语义,但是在每个键级别上。
public interface ConcurrentMap<…> extends Map<…> {
V compute(K key, BiFunction<…> remappingFunction)
V computeIfAbsent(K key, Function<…> mappingFunction)
V computeIfPresent(K key, BiFunction<…> remappingFunction)
…
}
这就是保持状态的类的样子。
public class TheStateHolder {
private final Map<Long, ClientBonus> state = new ConcurrentHashMap<>();
public ClientBonus nextState(Long client, Bet bet) {
return state.computeIfPresent(
client,
(k, currentState) -> currentState.nextState(bet));
}
它包含ConcurrentHashMap,并且每次应用程序获取新数据时,它都仅以原子方式计算新状态。
在我们的案例中,我们决定将ClientBonus本身作为计算新状态的对象,
public class ClientBonus {
...
public ClientBonus nextState(Bet bet) {
...
}
…
因此nextState函数必须是纯函数。
因此,通过这种方式,我们设法将状态管理与应用程序逻辑分开。
Effects
因此,既然我们知道如何与State战斗,那么让我们来看看我们可以用效果做什么。
效果是我们的应用程序必须执行的操作,以更改外部世界的状态。
在我们的案例中,这些影响是诸如向用户发送有关奖金进度的通知,或将价格支付到客户的帐户中。
Clojure与Java一样,不是像Haskel那样的纯语言,因此它实际上不提供任何用于处理IO的特殊工具。 现在让我们看看如何处理效果。
通常,在我们的应用程序中,我们会有类似的内容。 一些服务依赖于某些接口的对象,然后在运行时注入一些依赖项。
如果您考虑这种服务的外观,您会注意到它需要处理两件事:它决定了我们的应用程序必须执行的副作用,另外还必须执行这些副作用并处理任何可能的错误,或者 通过执行效果而引发的异常。 因此,当您为服务编写代码时,必须牢记这两件事。
因此,如果我们想采用一种更具功能性的方法,则希望将这两件事分开,以便我们可以独立地进行处理。 一方面,我们要决定需要执行哪些效果,另一方面,我们必须处理与外部世界进行交互的混乱而丑陋的细节。
要决定需要执行哪些效果,在我们的业务逻辑中,我们可以计算效果,而不是计算下一个状态。 这样,计算要执行的效果就成为我们纯业务逻辑的一部分,成为我们纯功能的一部分。
public class ClientBonus {
...
public Pair<ClientBonus,Effects> next(Bet bet) {
...
}
…
请注意,这也意味着我们的效果在我们的应用程序中成为明确的一流概念。
这将是一个类的示例,该类表示通知客户有关奖金进度的效果。
现在,即使系统的另一部分将执行此效果并处理错误,我们的业务逻辑仍可以决定如何处理效果和错误。
例如,在我们的业务逻辑中,我们可以将此效果包装在“忽略”错误策略中,而在其他情况下,也许可以确定正确的策略是停止JVM。
除错误策略外,应用程序逻辑还可决定是否必须按顺序运行效果,因此,如果其中一个失败,则将不执行其余操作。
或者效果可能是独立的,因此一个错误不会影响其他错误,这也可能意味着效果可以并行执行。
对于我们的红利申请,我们决定不要在效果中建立这些灵活性中的任何一种,因为我们认为这是没有必要的,相反,我们采用了一种非常僵化,非常静态的方式来定义效果,并在类型中进行编码 系统是什么有效和可能的影响链。
但是,既然我们已经描述了必须运行哪些副作用,我们仍然需要执行它们,我们仍然需要运行它们,因此需要一些代码来解释对影响链的描述。
在我们的案例中,由于此说明的结构非常严格,因此我们选择仅让每个效果都知道如何运行。
public interface Effect {
void run(AllDependencies dependencies);
}
请注意,正是在这里,我们传递了执行那些副作用所需的所有依赖关系,例如http或JMS客户端。
传递AllDependencies对象的好处是,它可以很明显地看出哪些方法是不正确的,因为要能够执行任何副作用,该方法将需要将该依赖对象声明为参数。
它的缺点是有时传递它有点麻烦,并且AllDependencies类非常丑陋,因为它必须持有并可以访问很多依赖项。 AllDependencies类几乎就像您的Spring Context。
因此,我们的代码如下所示:
Pair<ClientBonus, Effects> pair = theStateHolder.nextState(bet);
pair.effects.run(dependencies);
我们计算下一个状态和要执行的效果,然后执行这些效果。
但是问题是,此线程安全吗?
使用我们之前关于原子的规则,很明显不是这样,因为其中一个规则是,在计算新状态的纯函数之外发生的任何事情都可能成为竞争条件的对象。
比赛条件在这里吗?
因此,假设有两个线程来计算新状态和要执行的效果。
原子要确保这两个线程做出的决定是原子的,一致的和孤立的。 到现在为止还挺好。
但是,既然我们不在原子机制之内,那么我们可能会遇到竞争条件,因此线程2可能在线程1之前执行其副作用。
根据您的业务需求,这是否可以接受。
因此,原子并不能使所有的竞争条件消失,但它们应该使它们在何时发生的可能性上更为明显。
如果在您的应用程序中这种竞争条件是不可接受的,则可能的选择是使用代理。
Clojure代理基本上就像一个原子,但是它为您提供了单线程的额外保证。 如果您熟悉角色,则它们在并发模型中有点像角色。
对于我们的奖金申请,这种竞争条件是不可接受的,但是我们决定不使用Agent,而坚持使用原子,为什么呢?
好吧,我们正在运行多个Bonus Service实例,因此我们现在处于分布式系统编程领域。
由于无法以原子方式完成我们的应用程序需要执行的副作用,因此分布式系统理论告诉您,您必须在最少一次或最多一次语义之间进行选择。
在我们的案例中,我们查看每种效果,然后针对每种效果,我们决定哪种方法更合适。 对于那些至少一次的服务,我们没有任何协调。
对于需要最多一次语义的数据库,我们使用关系数据库作为协调机制,因为DB提供了ACID保证。
因此,在执行一项最多需要一种语义的效果之前,应用程序将与数据库进行检查,因此,如果多个实例之间在执行相同效果方面存在竞争,那么只有一个实例可以继续执行。
请注意,它仍然是我们的纯函数,用于计算效果的函数,
那些决定何时和哪些效果需要至少一次或最多一次语义的方法,我们通过将这些效果包装在“最多一次”策略中来完成。
Co-Effects
副作用的最后一种类型是协同效应。 协同效应是我们的应用程序做出决策所需的输入,数据。
对于我们的红利申请,我们基本上需要4条信息:哪些客户签署了哪些红利,客户的下注和存款,以及由于客户获得赠金的时间有限,我们还需要 知道现在几点了。
如前所述,我们将所有状态都保存在内存中,并且能够执行此操作,因为客户端事件的输入源是Kafka。 如果您不熟悉Kafka,则可以像一个不可变的消息队列那样思考它,它会记住所有经过它的消息。
这样,当奖金应用程序启动时,它将向Kafka询问最近几个月的所有消息,并从所有这些事件中重新计算当前状态。 同样,每个事件都带有时间戳,因此应用程序将事件时间用作其逻辑中的当前时间。
这基本上是事件来源。 从本质上讲,事件源和功能编程有很多共同点。
事件源和功能编程是并驾齐驱的。
Benefits
如果把所有东西放在一起,这就是整个样子
public class KafkaConsumer {
private AllDependencies allDependencies;
private TheStateHolder theStateHolder;
public void run() {
while (!stop) {
Bet bet = readNext();
Effects effects = theStateHolder.event(bet);
effects.run(allDependencies);
}
}
…
}
这是两个将由您选择的依赖项注入框架注入的依赖项。
具有效果所需的所有依赖关系的对象以及应用程序的状态。
在这里,我们使用的是Kafka轮询api,因此KafkaConsumer将是一个线程,它将读取Kafka主题中的新事件。
然后要求我们的状态提前时间,更新状态并返回我们需要执行的效果。
最后,我们要求效果自行执行。
通过遵循这种方法,我们的代码会发生一些有趣的事情:
首先,我们的业务对象具有0个getter或setter。
而且,我们的业务逻辑更加简洁,因为它没有锁或同步方法,没有try / catch块,也没有日志记录,因为所有这些都将在系统的不同部分完成。 这消除了业务逻辑中的许多噪音。
而且,在单元测试中没有模拟,因为业务逻辑的输入和输出都使用纯值,因此我们的单元测试更加简单。 为了测试副作用,包括代码库中所有不正确的部分,我们决定使用少量的全栈或集成测试。
最后,由于我们不必模拟任何东西,因此在代码库中没有任何无用的接口。 所谓无用的接口,是指那些只有一个生产实现的接口。
Functional core, Imperative shell
现在,这种设计或体系结构样式称为功能核心,命令式外壳。
功能核心是我们所有纯功能的生存所在,没有副作用。 功能核心是我们尝试做出尽可能多的决定的地方,因为它更易于测试和更改。
命令式外壳是所有副作用都存在的地方,是有关错误处理,状态和IO的所有丑陋代码。 我们试图避免命令式外壳出现任何条件,任何决定。
目的是尝试使功能核心尽可能大,同时使命令性外壳尽可能薄。
由于还有其他具有相同圆形形状的体系结构,因此我想说得很清楚。
如果您有这样的代码,您的核心类也依赖于核心程序包中的某些接口,然后在运行时注入实际的实现,则ClientBonus中的这段代码将无法正常工作。 您的功能核心不能依赖任何可能产生副作用的代码,甚至不能间接依赖。
现在,我并不是说您不应该这样做,而是要指出的是,当您执行此操作时,所有这些代码都属于命令性外壳,因此您不会获得功能核心的好处。
Dynamic (vs Static) typing
让我们谈谈下一个大差异。 Clojure是一种动态语言,而Java是一种静态语言。
典型的Clojure程序如下所示:
clientBonus = Map.of(
"client", Map.of("id", "123233"),
"deposits",
List.of(
Map.of("amount", 3,
"type", "CASH"),
Map.of("amount", 234,
"type", "CARD")));
((List) clientBonus.get("deposits"))
.stream()
.collect(
Collectors.summarizingInt(
m -> (int) ((Map) m).get("amount")));
首先,我们的域对象只是一堆地图和列表。 然后,我们的业务逻辑就是处理这些地图和列表。
我不确定您对此有何看法,但出于Java的敏感性,
这只是地狱,此代码是不可维护代码的确切定义。 如果团队中有人写过这段代码,我会要求他们很好地解释为什么这么做。
因此,在我们的奖金应用程序中,我们决定完全不执行此操作,因此我们没有带来Clojure的动态类型,而仅使用Java类型系统。
但是令人惊讶的是,当您编写Clojure代码时,这种动态类型化不再是一个问题,而且我认为这是因为Clojure核心api是专门为使用这种动态数据结构而设计的,因此与Java API相比,麻烦程度要小得多 。
但是,一旦编写了足够的Clojure代码,您的头脑就会开始出现功能障碍,然后当您回到Java时,您会开始真正地产生怪异的想法。
因此,当您输入此类时,
public class Bet {
private String id;
private int amount;
private long timestamp;
}
您开始怀疑,创建一个新类的价值是什么? 我能从中得到什么而不是使用普通地图?
您会注意到,获得的第一件事是几乎没有用的toString方法,同时还得到了一个损坏的equals和hashCode实现。 确实很烦人,但至少我们有龙目岛。
但是你输了什么? 突然,您失去了地图附带的所有功能,但是,更糟糕的是,您拥有的所有与地图一起使用,能够理解地图的代码都不适用于这个新类。 Java核心API中没有可用此类进行任何操作的代码。 除了反射API。
此外,您要在Github中找到多少个与此新类一起使用的库? 没有。
此时,您开始了解Alan Perlis的意思。
在一个数据结构上运行100个功能比在10个数据结构上运行10个功能更好。 艾伦·佩利斯(Alan Perlis)
每个新类都是一个新的数据结构,具有零功能,与任何其他代码完全隔离。 这妨碍了代码的可重用性。
但是,如果我们仅将Bet对象保留为纯数据怎么办?
{:type :bet
:id "client1"
:amount 23
:timestamp 123312321323}
您有一个明智的toString,您在那里可以看到。
您还可以免费获得适当的equals和hashCode。
但是更重要的是,您仍然可以使用编程语言随附的所有核心功能,因此您不必从头开始,可以重用大量代码。 您会发现可以使用此代码的Github库。
Clojure社区已经接受了使用纯数据表示尽可能多的事物的想法。
例如,您可以使用纯数据表示http请求,因此您的http服务器要做的就是将此映射用作输入
{:request-method :get
:uri "/foobaz"
:query-params {"somekey" "somevalue"}
:headers {"accept-encoding" "gzip, deflate"
"connection" "close"}
:body nil
:scheme :http
:content-length 0
:server-port 8080
:server-name "localhost"}
并生成另一个地图作为输出。 您将使用相同的核心API来完成此操作。
{:status 200
:headers {"Content-Type" "text/html"}
:body "Hello World"}
想想您的测试将变得多么容易。
而且,您还可以将其他内容表示为纯数据。
SQL查询:
{:select [:id :client :amount]
:from [:transactions]
:where [:= :client "a"]}
和数据库结果集:
[{:id 1 :client 32 :amount 3}
{:id 2 :client 87 :amount 7}
{:id 3 :client 32 :amount 4}
{:id 4 :client 40 :amount 6}]
HTML和CSS:
[:html
[:body
[:p "Count: 4"]
[:p "Total: 20"]]]
组态:
{:web-server {:listen 8080}
:db-config {:host "xxxx"
:user "xxxx"
:password "xxxx"}
:http-defaults {:connection-timeout 10000
:request-timeout 10000
:max-connections 2000}
:user-service {:url "http://user-service"
:connection-timeout 1000}}
甚至有关数据的数据,元数据:
{:id :string
:name :string
:deposits [{:id :string
:amount :int
:timestamp :long}]}
因此,通过拥抱使用纯数据的想法,您最终将使用相同的核心API来编写
- 您的业务逻辑您的基础架构代码您的配置您的元数据。
您只需学习和掌握一个API。
因此,Clojure中的动态键入并不像您期望的那样糟糕,因为它带来了很多好处。
Dynamic (vs Static) development
但是类型只是Clojure和Java之间的动态与静态差异之一。 Clojure提供了动态的开发经验。 这是什么意思? 在Clojure中,必须开发新功能时要做的第一件事就是启动应用程序,然后要做的就是不断更改正在运行的应用程序,直到完成为止,而无需停止它。
您可以通过使用REPL来实现。
当然,Java现在有一个称为REPL的东西,但是
仅仅因为它们具有相同的名称,并不意味着它们是相同的。
使用适当的REPL,您将永远不会构建或启动应用程序,而只能从内部扩展应用程序。
适当的REPL给您带来与Unix Shell相同的感觉,相同的人体工程学。
适当的REPL就像一直在运行JVM的调试器一样。
适当的REPL是测试驱动开发工作流程中缺少的部分。
Ťhis talk is my best attempt to explain what a REPL is, but I think that a REPL is one of this very very alien things, that you really need to experience it, because it is very hard to understand or imagine.
使用Java时,我最想念的是适当的REPL。
Lisp (vs Fortan)
好的,演讲的最后一部分。
对于我们的奖金项目,我们显然没有使用Clojure语法,因为如果我这样做了,我就不会在这里发表演讲。
但是对于所有人,每当您看到Lisp时都会尖叫起来,我对您来说是个好消息。
首先,与其他现代JVM语言一样,在Clojure中,您不必键入分号! 我认为我们都同意,这是对Java的巨大改进。
实际上,此功能非常强大,极大地提高了生产率,以至于Clojure更进一步,在Clojure中,逗号是可选的! 考虑一下您键入的所有数以百万计的逗号。
想象一下,如果您能把所有的时间都拿回来,我至少要年轻20岁。
但是我知道你在想什么
Lisps臭名昭著的所有括号怎么办? 好吧,即使在这里,我也对你有个好消息。
.filter(removeCsvHeaders(firstHeader))
.map(splitCsvString())
.map(convertCsvToMap(csvHeaders))
.map(convertToJson(eventCreator))
(filter not-header?)
(map parse-csv-line)
(map (partial zipmap headers))
(map ->event)
那是我的一个团队的两段代码。 在学习Apache Spark时,我们碰巧用Clojure和Java编写了基本相同的应用程序。 这是应用程序的主要逻辑,如您所见,它们是相同的,但是有一个重要的区别。
让我们计算一下括号。 1、2、3…Java版本有16个括号。 Clojure有多少个呢? 10.因此Clojure版本的括号减少了40%。
不仅如此,该应用程序的Clojure版本具有代码的十分之一。
十分之一,想象一下是否可以删除90%的代码。
好吧,足够愚蠢的笑话。 让我们看看为什么Lisp的人们如此钟爱括号。 为此,我非常抱歉,但是我将不得不向您展示更多Clojure代码。
List.of(
new Symbol("defn"),
new Symbol("plus-one"),
List.of(
new Symbol("a"),
new Symbol("b")),
Map.of(
new Keyword("time"), List.of(new Symbol("System/currentTimeMillis")),
new Keyword("result"), List.of(
new Symbol("+"),
new Symbol("a"),
new Symbol("b"),
new Long(1))));
这是典型的Clojure程序。 我们正在定义一个函数,该函数需要两个参数,并返回一个映射图,其中包含这些参数的和。
好的,也许Clojure的详细程度要比这少一些,但这实际上是您在编写和键入Clojure时所做的工作。 这是什么? 您的代码只是列表和地图,这就是当我们在Lisp中说代码是数据时的意思,因为从代码上看,它是实际数据。
而且由于它是数据,因此我们可以使用与用于业务逻辑,基础结构代码和配置的工具完全相同的API来对其进行操作,生成,分析。
元编程(即编写程序的程序)成为处理列表和映射的问题。 这很简单,但功能非常强大。
这就是为什么Lispers非常喜欢括号的原因。
Summary
所以总的来说...
尝试编写尽可能多的纯函数,它们会使您的应用程序更易于理解和更改。
使用Clojure之后,我将动态类型与静态类型作为权衡取舍。 的确,在Clojure中,我错过了良好的Java IDE所具有的一些重构功能,并且有时我会浪费时间去追求一些拼写错误的词,但是Clojure专注于数据在某种程度上使交易失去了公平。
但是,在享受Clojure的动态开发经验之后,我永远都不会放过这一点。
并且请不要害怕括号。 没有IDE就不会编写Java的方式相同,没有IDE就不会编写Clojure。 IDE将处理所有这些令人恐惧的括号。 并记住,有一个非常好的理由。
我想以Alan Perlis的另一句话作为结尾:
不影响您对编程思维方式的语言是不值得了解的。
对我来说,Clojure就是其中一种语言。 默认情况下,不变性,函数式编程,动态类型以及repl,Lisp语法和宏,一切都作为简单数据。
所有这些对我来说都是重要的教训。 他们改变了我解决问题的方式,改变了我构建应用程序的方式,改变了我设计系统的方式。
但是这些都不是Clojure最重要的一课。
在我学习Clojure的过程中,最重要的见解是最深刻的教训,这是我一直对不同的想法持开放的态度,只是因为它们与我以前的习惯不同。
如果你们中的任何人在5年前或6年前告诉我学习动态的口吻,我会说“没有办法,我不会浪费时间”。 但是,我在这里宣讲Clojure。
Clojure敞开心my,对不同的想法感到好奇,即使那些最初看起来似乎令人恶心的想法也是如此。
因此,我想鼓励大家在今年或明年学习一种新的语言,而不必是Clojure,
但选择与您习惯完全不同的东西,使您感到不安的东西,完全陌生的东西。
我相信在这段旅程中,您会学到一些您想带到日常工作中的东西。
在最坏的情况下
它只会使您变得更怪异,更难以与他人建立联系。
非常感谢您的时间。
但是在您离开之前,请快速浏览以下视频: