6 年专业Clojure经验分享 - Erez


  • Clojure 是一种很棒的编程语言,因为它具有函数性、缺乏对象/对原始值的关注以及通过其无缝 Java 互操作提供的庞大 JVM 生态系统
  • 与其他编程语言相比,Clojure 工程师的招聘和构建工程团队具有挑战性,因为它不受欢迎,并且缺乏大量经验丰富的工程师

在主要与 Ruby 合作多年后,我来到了Nanit。那时我并不真正了解 Clojure,所以在我的第一阶段,我主要做 Ruby 工作以提供快速价值。从那以后 6 年多过去了,今天 Clojure 是我工具箱中最强大的工具之一,也是我觉得最高效的语言。
在这些年里,Nanit 的后端团队变得越来越大,关于选择 Clojure 作为我们主要编程语言的问题一再出现,这主要是因为缺乏经验丰富的 Clojure 工程师。
当我试图为这个问题提供答案时,我总是觉得我必须回忆我的想法并将它们组织成连贯的论点,尽管 Clojure 的优势一直对我来说非常清楚。我决定有一天我会在一篇博客文章中表达我对 Clojure 的看法。这一天到了:)
我总是喜欢说,尽管我已经与 Clojure 一起工作了五年多,但我绝不是“Clojure 专家”。因为我认为自己是一个倾向于深入研究主题的人,所以我认为这更多地反映了 Clojure 作为一种语言而不是作为软件工程师的我。只是与其他编程语言相比,Clojure 相当简单,简化一个主题,使专业知识变得深奥。换句话说,Clojure 允许您以很少的知识实现很多,因为要知道的并不多,这真的很棒。
不应将简单与软弱混淆。相反,Clojure 的简单性是它的主要优势,因为您可以实现使用其他语言(如 Ruby、Java 或 Python)所能实现的一切,而代码中的开销和意外复杂性更少。
我想尽量避免“语言战争”,得出一个绝对的结论,即 Clojure 是地球上最好的语言。Clojure 是我工具箱中的另一个工具,可能比其他用例更适合某些用例。相反,我将尝试列出使我在使用 Clojure 时更轻松的客观参数,以及一些我在使用 Clojure 作为语言在技术上遇到困难的主题,以及构建一个主要将 Clojure 作为他们的工具实践的软件工程师团队。
 
函数式编程
Clojure 是一种函数式编程 (FP) 语言。对我来说,作为一个软件开发者,FPs 最大的优势就是大部分的代码库都是由“纯函数”组成的。纯函数有两个特性,使它们更容易测试、重构和组合成更复杂的函数:
  1. 它们没有副作用。副作用包括网络 IO、磁盘交互或改变系统状态。
  2. 他们的输出完全依赖于他们的论点。它们不依赖于外部状态来计算它们的返回值。

我创建软件分为4个主要活动:
  • 我读了现有的代码,并试图了解它
  • 我重构,需要重构代码
  • 在写新的代码之前设计
  • 我写新代码的测试— 此代码可能重复使用现有代码

上述两个特征的组合使我更容易列出任何列出的活动:
  1. 纯函数使代码设计更容易:事实上,当您的代码库主要由纯函数组成时,几乎不需要进行任何设计。您不必使用接口、扩展和实现来构建类层次结构。不需要像继承上的组合或访问者模式这样的高级设计技巧。您不必为多重继承问题或可怕的菱形图找到创造性的解决方案。在过去的 6 年里,我没有处理过任何这些问题,但我编写了精心制作、经过测试、可维护、可读、可扩展的生产级代码(或者至少这是我愿意相信的 :))。
  2. 纯函数更易于重用:我可以根据需要多次使用纯函数,而无需考虑它如何影响系统,因为没有任何副作用。这就像计算机编程的 WYSIWYG——函数遵循它的主体而不是其他任何东西。无需考虑任何隐藏的考虑因素。纯函数通过消除必须调查我要重用的代码是否会影响系统以及如果是的话会产生什么影响的额外开销来鼓励代码重用。
  3. 纯函数更易于阅读 和 理解:每个纯函数都是一段孤立的、一致的和可预测的代码,仅依赖于其参数。您不需要熟悉数据库模式或 RabbitMQ 架构来推理代码——这完全是关于在函数体中完成的参数和数据转换。
  4. 纯函数更容易测试:由于它们不依赖于外部状态,因此您测试函数所需要做的就是将其应用于其参数。无需在数据库上创建夹具或模拟 HTTP 请求。此外,由于纯函数不会对系统应用任何更改,因此您只需测试返回值。
  5. 纯函数更容易重构:它们缺乏外部依赖和无状态,将它们变成了一个独立的构建块,易于替换和组合。

 
只有值,只有基元
Clojure 没有“对象”。我的意思是,确实如此,但大多数时候你不会觉得需要这些。相反,Clojure 依赖于原始值和它们的集合(数组、字典、集合等)。我在 Clojure 中所做的 99% 都是使用包含原始值的数组和字典。
作为一名软件工程师,处理原始值对我来说更容易:
  1. 我的代码侧重于业务逻辑和数据转换,而不是描述域及其关系。每行代码都在执行业务逻辑,因此业务逻辑在整个代码库中非常突出。
  2. 我不必熟悉数百个独特的对象和编码到它们中的行为才能有效:传入的 HTTP 请求?它是一个普通的 Clojure 字典。你想形成一个 SQL 查询?构建一个字典并将其传递给 SQL 库进行格式化。您想返回 HTTP 响应吗?您返回一个包含状态码、标题和正文键的字典。想要从 RabbitMQ 队列中读取消息?是的,你猜对了——你得到了一本字典。如果您熟悉 Clojure 对其基本数据结构(如字典)的操作,您将在 HTTP、SQL、RabbitMQ 和系统的每个其他特定领域部分中变得有效。它将域中所需的复杂性和熟悉程度降低到最低要求,因为从软件方面来看,您所做的只是重复构建、转换和移动字典从一个功能到另一个功能。
  3. 测试变得更加容易,因为我不必模拟复杂的对象或创建所需接口的特定于测试的实现来允许代码运行。我所要做的就是创建数据这始终是一个Clojure的或多个图元的集合。

 
最少的语法
Clojure 的语法建立在它自己的数据类型之外。此属性称为同质性。起初听起来很奇怪,但我会尝试证明:
Clojure 向量(其他语言中的数组)如下所示[1 2 3 4]Clojure 列表如下所示:(1 2 3 4)
要定义一个函数,你会写:
(defn my-sum [arg1 arg2] (+ arg1 arg2))
如您所见,代码是一个 Clojure 列表,其中包含符号defn、函数名称和参数向量。主体是一个列表,其中函数作为第一个成员 (+),后面是参数。
为什么这是一件好事,你可能会问自己?好问题!
  1. 通过宏生成代码感觉很自然。由于我们在 Clojure 中所做的大部分工作都是为了有利于业务逻辑而转换和生成数据结构,因此使用相同的数据结构执行相同的操作来生成代码几乎不会引起注意。
  2. 它将您必须熟悉的特殊符号和字符的数量减少到最少。代码和数据合二为一,因为它们共享相同的数据结构、行为和语法。

 
并发
使用 Clojure 时,并发感觉不是问题,主要有两个原因:
  1. Clojure 的大部分值都是不可变的,这可以防止竞争条件并允许代码不受互斥锁和锁等共享访问控制的影响。那些不是一成不变的(例如原子)提供了操作它们存储的数据的安全方法。
  2. Clojure 有大量用于并发编程的工具,称为clojure.async。至少从我的经验来看,这些工具的亮点是 Channels,它允许在一组通道上进行安全的线程间通信和选择,就像Golang 的 select 指令一样

 
Java互操作
Clojure 不是一种广泛使用的编程语言,因此,常见用例缺少许多库。幸运的是,Clojure 与 Java 的互操作是无缝的,因此在实践中,Java 的庞大生态系统触手可及。通过这种方式,您可以享受使用 Clojure 的乐趣,但不会受到其缺乏流行度和库的影响。
 
缺点
是的,Clojure 很棒,但就像我们在生活中做出的大多数决定一样,使用 Clojure 做出的决定也是权衡取舍。
Clojure 的第一个方面让我过得很艰难是 JVM,原因有以下 3 个:
  1. JVM 是一个众所周知的内存吞噬者,很难预测您的应用程序内存需求。此外,它似乎总是需要比运行应用程序所需的更多内存。我确信相同的应用程序在其他运行时会占用更少的内存(尽管我从未花时间证明这一点)。
  2. 调试远程服务器中的内存泄漏和堆大小非常困难。我们尝试了VisualVM,但由于 Clojure 内存主要由原语(字符串、整数等)组成,因此很难理解正在累积应用程序的哪些数据以及原因。我假设在基于 Java 的常见应用程序中,大部分内存由 Java 对象组成,因此内存分析会更容易。
  3. 随着项目规模的增长,Clojure 项目的启动时间可能会变得很长。尽管有GraalVM 之类的解决方案,我还没有机会在生产中体验它们以证明它们的成熟度和健壮性。

总而言之,我不是 JVM 的粉丝,但我确实理解将 Clojure 的运行时定位到 JVM 的决定背后的原因。

在大型的、不熟悉的 Clojure 代码库中工作时,我发现困难的第二个主题是Typing.
Clojure 是一种动态语言,它有它的优点,但当我偶然发现一个接收字典参数的函数时,我发现自己花了很多时间来找出它拥有哪些键。有时我不得不在我们的集成环境中放置一个日志,以查看它接收到什么消息以及该消息中有哪些字段可供我使用。
有时我会去测试该函数并查找我们在测试中使用的示例参数值,但这可能还不够,因为该字典中可能存在其他字段并且只是未在函数中使用时刻,因此它们也可能从测试值中丢失。有时我会查看函数的调用站点以了解传递了什么参数以及它是如何构建的。
也有解决方案,例如core.typed,但我自己从未体验过它们,我不确定它们的全面性和可用性。

使用 Clojure 的最后一件难事是招聘和入职,我已经在这篇文章的前面提到过。招聘很难,因为现有的 Clojure 工程师人数很少,而且一些工程师出于职业发展的考虑故意避免使用不受欢迎的语言。其他工程师获得了特定语言的专业知识,并希望继续使用这些语言,因此 Clojure 不是他们的选择。
 
结论
我认为每个软件工程师至少需要让他们自己熟悉一种函数式编程语言,才能敞开心扉,看看 OOP 范式之外的东西。学习 Clojure 让我怀疑我以前作为软件工程师实践过的一切,并就我如何将精力花在正确的方向上,为我工作的公司提供有价值的基础上提出问题。
我认为 Clojure 作为一种成熟的、可用于生产的、简单的编程语言,非常适合进行这种探索。您可以选择专业地使用它,用于业余项目或根本不使用它,但是让自己接触这种语言的经验肯定会丰富您对编程的看法并使您成为更好的开发人员。