首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

一文解决现代编程语言选择困难:响应式编程

如果搜索“最佳编程语言”,结果会罗列一堆文章。这些文章涵盖各主流语言,并且大多对各语言优缺点的表述模棱两可,表述不到位,缺少实战借鉴意义。本文概述了当前再用的现代编程语言,按推荐程度从低到高依次列出。希望本文有助于读者选择合适的工具完成工作,降低开发工作量。鉴于原文篇幅过长,译文按设计用于命令式编程的 C 语言家族,以及设计用于响应式编程的 ML 语言家族,分为上下两篇提供。本文是下篇(上篇:《一文解决现代编程语言选择困难:命令式编程》)。

函数式编程

开篇先介绍函数式编程,然后继续对语言做排名。为什么要考虑函数式编程?因为函数式编程给开发者带来了和谐与安宁。

函数式编程可能听起来有些高大上,但实际上无需过于担心。简而言之,函数式语言吸取了其他一些语言的经验教训,实现了许多正确的设计决策。在很多情况下,函数式语言提供正确的功能,包括支持 ADT 的强大类型系统、无空值、无需异常的错误处理、对不可变数据结构的内建支持、模式匹配和函数复合(compose)运算符。

做为本文评判的加分项,函数式编程语言提供哪些共性的优势?

使用纯函数的编程

不同于主流的命令式语言,函数式编程语言鼓励使用纯函数(pure function)编程。

什么是纯函数?该理念非常简单,即给定相同的输入,始终返回相同的输出。例如,2+2 始终返回 4,因此加法运算符“+”是纯函数。

纯函数不允许与外界交互,不支持 API 调用,无法写入控制台,甚至不允许更改状态。这完全不同于面向对象编程所采取的方法,即其中任何方法都可以自由地变更(Mutating)其他对象的状态。

纯函数和非纯函数非常易于区分。函数是否不带参数,是否不返回值?如是,则为非纯函数。

下面例子给出的是非纯函数:

代码语言:javascript
复制
// 非纯函数,根据随后的调用,返回不同的值。
// 要点:不带任何参数。
Math.random(); // => 0.5456412841544522
Math.random(); // => 0.7542151348966241
Math.random(); // => 0.4534865342354886


let result;
// 非纯函数,变更外部状态,即result变量。
// 要点:不返回任何值。
function append(array, item) {
  ​result = [ ...array, item ]
}

下面例子给出的是纯函数:

代码语言:javascript
复制
// 纯函数:不变更函数体外任何状态。
function append(array, item) {
  ​return [ ...array, item ]
}


// 纯函数:同样的输入,总是返回相同的输出。
function square(x) { return x * x; }

此类方法看上去难以理解,需要一段时间才能习惯。我一开始也对此颇感困惑!

那么纯函数有什么好处?它非常容易测试,无需 Mock 和 Stub;易于推断,不同于面向对象编程,无需牢记整个应用的状态。开发人员只需关注当前正在操作的函数。

纯函数可以轻松复合(compose)。由于纯函数之间不存在共享状态,因此非常易于实现并发。纯函数的重构可谓是件快事,只需复制和粘贴,完全可不借助于任何复杂的 IDE 工具。

简而言之,纯函数让编程回归欢乐。

函数式编程鼓励使用纯函数。如果 90%以上的代码库是由纯函数组成时,这当然很好。但是一些语言也走了极端,完全禁止使用非纯函数,这并非总是好事。

不可变数据结构

本文下面列出的所有函数式语言,均内建对不可变数据结构的支持。不可变数据结构是持久的,更改后不必创建整个数据结构的深层拷贝。设想一下,如果反复地对一个超 10 万项元素的数组做镜像拷贝,那么性能一定好不了。

持久数据结构无需创建拷贝,仅在简单重用旧数据结构引用的同时,添加所需的更改即可。

代数数据类型(ADT)

ADT 是一种强大的应用状态建模方法,类似于合成类固醇的方法构建枚举类型(Enums on steroids)。只需指定构成某个类型的可能“子类型”,以及子类型的构造函数参数。例如:

代码语言:javascript
复制
type shape =    ​| Square(int   ​| Rectangle(int, int   ​| Circle(int)

上例中,“shape”类型可以是 Square,、Rectangle 或 Circle。 Square 的构造函数使用单个 int 参数,即指定正方形的宽度;Rectangle 使用两个 int 参数,即指定长方形的宽度和高度;而 Circle 使用单个 int 参数,即指定圆的半径。

下面给出类似功能的 Java 实现代码:

代码语言:javascript
复制
interface Shape {}


public class Square implements Shape {
  ​private int width


  ​public int getWidth() 
    ​return width
  ​


  ​public void setWidth(int width) 
    ​this.width = width
  ​
}


public class Rectangle implements Shape {
  ​private int width
  ​private int height


  ​public int getWidth() 
    ​return width
  ​


  ​public void setWidth(int width) 
    ​this.width = width
  ​
  
  ​public int getHeight() 
    ​return height
  ​


  ​public void setHeight(int height) 
    ​this.height = height
  ​
}




public class Circle implements Shape {
  ​private int radius


  ​public int getRadius() 
    ​return radius
  ​


  ​public void setRadius(int radius) 
    ​this.radius = ra

相比之下,谁又会抗拒使用函数式语言的 ADT?

模式匹配

所有函数式语言都对模式匹配提供了强大的支持。模式匹配通常可编写出更具表现力的代码。

下面给出一个布尔选项类型的模式匹配例子:

代码语言:javascript
复制
type optionBool =
   ​| Some(bool
   ​| None


let optionBoolToBool = (opt: optionBool) => {
  ​switch (opt) 
  ​| None => fals
  ​| Some(true) => tru
  ​| Some(false) => fals
  ​
};

下面给出未使用模式匹配的相同功能代码:

代码语言:javascript
复制
let optionBoolToBool = opt => {
  ​if (opt == None) 
    ​fals
  ​} else if (opt === Some(true)) 
    ​tru
  ​} else 
    ​fals
  ​
}

毫无疑问,使用模式匹配的代码更具表现力,更为简洁。

模式匹配还确保提供编译时的详尽信息,以免开发人员忘记检查各种可能情况。非函数式语言没有提供此类保证。

空值

在函数式编程语言中,通常避免使用空引用。类似于 Rust 那样,使用 Option 模式替代。例如:

代码语言:javascript
复制
let happyBirthday = (user: option(string)) => {
  ​switch (user) 
  ​| Some(person) => "Happy birthday " ++ person.nam
  ​| None => "Please login first
  ​}
};

错误处

通常并不建议在函数式语言中使用异常。同样,类似于 Rust,使用的是 Result 模式。例如:

代码语言:javascript
复制
type result('value, 'error) =
  ​| Ok('value
  ​| Error('error)


let happyBirthday = (user: result(person, string)) => {
  ​switch (user) 
  ​| Ok(person) => "Happy birthday " ++ person.nam
  ​| Error(error) => "An error occured: " ++ erro
  ​}
};

对于函数式错误处理,在“Composable Error Handling in OCaml”一文中给出了很好的说明。

Pipe forward 操作符

如果没有 pipe forward 运算符,函数调用难免会存在嵌套,这会降低了代码的可读性。例如:

代码语言:javascript
复制
let isValid = validateAge(getAge(parseData(person)));

函数式语言专门提供了管道运算符,简化了编程。上面代码可重写为:

代码语言:javascript
复制
let isValid =  ​perso    ​|> parseDat    ​|> getAg    ​|> validateAge

Haskell

Haskell 完全可称为所有函数式编程语言的“鼻祖”。Haskell 已 30 多岁了,甚至比 Java 还要老。函数式编程中的许多最佳创意,都源于 Haskell。

语言家族:ML

👍 👍 类型系统

Haskell 的类型系统比其他任何语言都要强大。当然,Haskell 支持 ADT,也支持类型类(Typeclass)。其类型检查器几乎可完成所有推断。

👎👎 学习难度

非常难!众所周知,要有效地使用 Haskell,首先必须精通范畴学,这并非开玩笑。面向对象编程中需要多年的经验,才能写出良好的代码。而学习 Haskell 则需要投入大量的精力,才能富有成效。

即便是使用 Haskell 编写一个简单的“Hello world”程序,也需要了解 Monads,尤其是 IO Monads。

👎👎 社区

根据我自身的经验,Haskell 社区更具学术性。例如在 Haskell 软件库邮件列表中,一个最新帖子的开头是这样说的: “私人通讯指出,元组函数\x->(x,x)实际上是对 biapplicative 及其相关结构所做的一种特殊单调(monadicially)对角化。” 该帖子讨论热烈,得到 39 个答复。 —— Hacker News上用户 momentoftop 的发言。

上面引用的内容,很好地评价了 Haskell 社区。Haskell 社区更喜欢开展包括范畴论在内的学术讨论,而非去解决实际问题。

👎 函数纯度

上文介绍过,纯函数优点多多,但也存在一些副作用。例如,与函数体外的互动,包括变更状态(mutating state)。这些副作用是导致程序出现大量错误的重要原因。作为纯函数式语言,Haskell 完全禁止使用这些副作用。这意味着函数永远不能变更任何值,甚至不允许与函数体外进行任何交互。从技术上来讲,甚至不允许记录日志等的操作。

当然,Haskell 也提供了与外界交互的解决方法。其运作机制是通过提供一组称为 IO Monad 的指令。此类指令可提出:“读取键盘输入,然后在某些函数中使用该输入,然后将结果打印到控制台。” 之后,语言运行时会接受此类指令并执行。开发人员永远不会执行直接与外界交互的代码。

不惜一切代价避免成功! —— Haskell 的非官方座右铭

事实上,对函数纯度的关注,显著地增加了抽象的数量,进而增加了复杂性,降低了开发人员的生产效率。

👍 空值

和 Rust 类似,Haskell 不支持空引用。Haskell 使用 Option 模式声明可能不存在的值。

👍 错误处理

尽管一些函数可能会抛出错误,Haskell 代码的惯用模式类似于 Rust 中 Result 类型。

👍 不可变性

Haskell 对不可变数据结构提供一等支持。

👍 模式匹配

Haskell 提供很好的模式匹配支持。

👎 生态系统

Haskell 的标准库完全不成体系,尤其是默认的核心软件库 Prelude。Haskell 默认使用抛出异常的函数,而不是采用函数编程标准做法的返回选项值。更糟的是,Haskell 具有两个包管理器,Cabal 和 Stack。

评判

硬核(hardcore)函数式编程需要深度理解过多的高度抽象概念,因此永远不会成为主流。 —— David Bryant Copeland,《软件设计的四项良好原则(Four Better Rules for Software Design)》

我的确非常欣赏 Haskell,但不幸的是,Haskell 可能永远局限于学术界。Haskell 是最糟糕的函数式编程语言吗?各位自行判定,但我认为是。

OCaml

OCaml 是一种函数式编程语言。OCaml 是“Object Caml”的缩写,但讽刺的是,很少有人在OCaml中使用对象

OCaml 的历史几乎和 Java 一样长,“O”很可能是来自于那个年代“对象”这一潮流。OCaml 只是填补了Caml的空白。

语言家族:ML。

👍 👍 类型系统

OCaml 的类型系统可与 Haskell 相媲美。最大的缺点是缺少类型类(Typeclass),但支持高阶模块函子(functor)。

OCaml 是静态类型的。具有近乎 Haskell 的完美类型推断。

👎👎 生态系统

OCaml 的社区并不大。这意味着开发人员无法针对一些通常用例找到高质量的软件库。例如,OCaml 缺少适用的 Web 框架。

相比其他语言,OCaml 软件库的文档质量堪忧。

👎 工具

OCaml 的工具颇为混乱,并存在三种软件包管理器:Ppam、Dune 和 Esy。

OCaml 编译器错误信息非常不友好。虽然这并非致命因素,但的确令人沮丧,会影响开发人员的生产效率。

👎 学习资源

想要上手学习 OCaml 的话,这里推荐《Real World OCaml》一书。但该书自 2013 年后就再未更新,其中得许多例子已经不合时宜。该书中的操作已不适用于现代工具链。

相比其他语言,OCaml 教程可以说质量堪忧。大多只是学校课程的讲稿。

👎 并发

“多核随处可见(Multicore is coming Any Day Now™️)”这一口号,是 OCaml 并发历程的很好总结。OCaml 开发者多年来一直在苦苦等待适用的多核支持,但看来近期也不会添加到该语言中。OCaml 应该是唯一缺少良好多核支持的函数式语言。

👍 空值

OCaml 不支持空引用,使用 Option 模式声明可能不存在的值。

👍 错误处理

OCaml 代码惯用 Result 类型模式。

👍 不可变性

OCaml 对不可变数据结构提供一等支持。

👍 模式匹配

OCaml 提供很好的模式匹配支持。

评判

OCaml 是一种很好的函数式语言,主要缺点是对并发支持不好,社区不大,因此生态系统过小,缺少学习资源。

考虑到上述不足,我并不推荐在生产环境中使用 OCaml。

Scala

Scala 是为数不多真正的多重编程范式语言(Multi-paradigm programming language)。Scala 同时很好地支持面向对象编程和函数式编程。

语言家族:C

👍 生态系统

Scala 运行在 JVM 之上,因此可以接入 Java 的巨大生态系统。这极大地提高了开发人员后台的生产效率。

👍 类型系统

Scala 可能是唯一类型系统不健全(unsound)的有类型函数式编程语言,同时也缺少适当的类型推断。Scala 的类型系统比不上其他函数式语言。

好的一面,Scala 支持高级类类型( Higher-Kinded Types)和类型类(Typeclass)。

尽管存在不足,Scala 的类型系统依然值得肯定。

👎 代码简洁性和可读性

相比 Java,Scala 代码非常简洁,但可读性一般。

Scala 是为数不多属于 C 语言家族的函数式编程语言。C 语言家族设计为命令式编程,而 ML 家族语言设计为函数式编程。因此,使用 Scala 的类 C 语法进行函数式编程,时常看起来有些奇怪。

Scala 中没有适当的 ADT 语法,对代码的可读性产生了不利的影响:

代码语言:javascript
复制
sealed abstract class Shape extends Product with Serializable


object Shape {
  ​final case class Square(size: Int) extends Shap
  ​final case class Rectangle(width: Int, height: Int) extends Shap
  ​final case class Circle(radius: Int) extends Shap
}

而使用 ReasonML 的 ADT 编写为:

代码语言:javascript
复制
type shape = 
   ​| Square(int
   ​| Rectangle(int, int
   ​| Circle(int)

在可读性上,ML 家族语言的 ADT 明显胜出。

👎 👎 速度

Scala 可能是本文所列出语言中编译速度最慢的。一个简单的 Hello World 程序,如果不使用最新的硬件,编译可能需要 10 秒。Scala 编译不是并发的,使用单线程,编译速度不佳。

Scala 运行在 JVM 上,这意味着程序启动所需时间更长。

👎 学习难度

Scala 具有很多特性,导致学习难度高。该语言中充斥着各种特性。

Scala 可能是第二难学的函数式语言,仅好于 Haskell。事实上,很多企业放弃 Scala 的原因之一就是非常难学。

👍 不可变性

Scala 使用案例类(Case Class),对不可变数据结构提供一流支持。

👌 空值

不好的一面,Scala 支持空值。好的一面,Option 模式是潜在缺失值的惯用方式。

👍 错误处理

和其他函数式语言一样,Result 模式是 Scala 的惯用错误处理方式。

👌 并发

Scala 运行在 JVM 上,并非完全设计用于并发。好的一面,其Akka工具集非常成熟,JVM 提供类似 Erlang 的并发。

👍 模式匹配

Scala 提供很好的模式匹配支持。

评判

我想要去喜欢 Scala,但确实做不到。Scala 想要做太多的事情,为了同时支持面向对象编程和函数式编程,其设计者不得不做出权衡。正如俄罗斯谚语所说:“追逐两个兔子的人,最终一无所获”。

Elm

Elm 是一种编译为 JavaScript 的函数式语言,主要用于前端 Web 开发。

Elm 的独到之处,是其承诺不会出现运行时异常。Elm 编写的应用非常稳定。

语言家族:ML

👍 很好的错误信息

Elm 编译器提供的一些错误信息,是我看到的最到位信息。这使得该语言即便是对完全小白也非常友好。

👍 错误处理

Elm 是纯函数式语言,没有运行时错误,也不支持异常。这意味着如果一个代码库是 100%由 Elm 实现的,那么就永远不会出现运行时错误。Elm 可能出现运行时错误的唯一情况,是与外部 JavaScript 代码交互时。

Elm 是如何处理错误的?和其他函数式语言一样,使用 Result 数据类型。

👎 函数纯度

和 Haskell 一样,Elm 是纯函数式语言。

Elm 是否因为消除了所有运行时异常而提高了生产率,还是因为在所有地方强制函数式纯度而降低了生产率?从我个人经验看,对 Elm 代码的任何显著重构都涉及大量“疏通工作”,完全是灾难性的。

具体取决于开发人员,但我还是要给 Elm 一个差评。

👎 过于自行其是(opinionated)

Elm 是一种自行其是的语言。即便是制表符的使用,也会报语法错误。

Elm 对“从不报错”的追求,会导致该语言走上末路。其最新版 0.19 引入了突破性更改,导致 Elm 几乎无法与 JavaScript 软件库互操作。当然,这一更改的目的是让开发人员使用 Elm 编写自己的软件库,推动自身生态系统的发展。但是,几乎很少企业能有资源使用 Elm 重新实现所有一切。这会导致人们担心进一步发展而弃用 Elm。

看上去 Elm 的设计者过于注重函数纯度,在“永不报错”理念上钻了牛角尖。

👎 非 React

不同于 ReasomML 等语言,Elm 使用自己的 Virtual DOM,不使用 React。这意味着开发人员不能访问针对 React 制作的软件库和庞大组件生态。

👎 👎 语言发展

即便是 Elm 的最新版本 0.19.1,也已经发行已经一年多了。不透明的开发过程,导致他人难以对 Elm 的发展做出贡献。Elm 的每个主版本都引入了突破性更改,导致部分开发人员无法继续使用该语言。我们近一年多没有听到 Elm 创建者的任何消息,甚至不知道他是否依然全职维护 Elm。可能该语言已经死亡。

👍 模式匹配

Elm 提供很好的模式匹配支持。

👍 不可变性

对不可变数据结构提供一等支持。

👍 空值

Elm 不支持空引用。和其他函数式语言一样,Elm 使用 Option 模式。

评判

Elm 是一种优秀的语言。不幸的是,Elm 看上去并没有什么未来。但可作为入门函数式编程的很好途径。

扩展阅读:

F#

F#可视为 OCaml for .NET,其语法非常类似于 OCaml,只有几处细微修改。F#在 2005 年首次推出,目前是一种非常成熟的语言,具有很好的工具和丰富的生态系统。

语言家族:ML。

👍 👍 类型系统

F#类型系统的唯一缺点是缺少高阶类型(Higher-Kinded Types)。尽管如此,其类型系统依然是非常可靠的,编译器几乎支持所有推断。F#对 ADT 有很好的支持。

👍 函数式,但并非纯函数

不同于 Haskell 和 Elm,F#非常务实(pragmatic),并不强制纯函数式。

👍 学习资源

F#具有可媲美 Elixir 的很好学习资源

👍 学习难度

F#非常易于学习,是可供上手的函数式语言。

👌 生态系统

F#社区规模相当小,根本没有可与 Elixir 等语言媲美的软件库。

👍 与 C#的互操作(interop)

好的一面是,F#可以接入整个.Net 和 C#生态系统。可与现有 C#代码互操作。这是很大的优点。

👌 并发

F#运行在 CLR 之上,无法与 Elixir 通过 Erlang VM 提供的并发支持相媲美。

👍 空值

F#代码中通常不使用空值。F#使用 Option 模式定义声明不存在的值。

👍 错误处理

F#代码惯用 Result 类型实现错误处理。

👍 不可变性

F#对不可变数据类型提供一等支持。

👍 模式匹配

F#提供很好的模式匹配支持。

评判

F#具有非常好的类型系统,是一种非常可靠的编程语言。F#几乎和本文稍后介绍的 Elixir 一样,可用于 Web API 开发。但是,F#的问题在于它所不具备的特性。相比 Elixir,Elixir 的并发功能、丰富的生态系统和令人惊叹的社区,要胜过 F#静态类型所提供的好处。

扩展阅读:

奖项

F#获得两项荣誉。

  • “金融科技最佳语言”奖。众所周知,金融领域是 F#的主要应用。
  • “最佳企业软件语言”奖。F#的丰富类型系统支持对复杂商业建模。推荐阅读《Domain Modeling Made Functional》一书。

ReasonML

ReasonML 是一种编译为 JavaScript 的函数式语言,主要用于前端 Web 开发。

ReasonML 并非一种新的语言,而是老旧语言 OCaml 的新语法。ReasonML 由 Facebook 支持。

借助于 JavaScript 的生态系统,ReasonML 避免了 OCaml 的缺点。

语言家族:ML

👍 非 JavaScript 的超集

ReasonML 的语法类似于 JavaScript,因此有 JavaScript 开发经验的人可轻松上手。但是不同于 TypeScript,ReasonML 甚至并未考虑做为 JavaScript 的超集。我认为这是件好事情,ReasonML 没有继承数十年来 JavaScript 的不好设计理念。

👍 学习难度

鉴于 ReasonML 并未考虑做为 JavaScript 的超集,因此语言上要比 JavaScript 简单很多。具有 JavaScript 函数式编程经验的人,可在一周内轻松上手。

ReasonML 的确可称为本文列出所有语言中最简单的。

👍 函数式,但并非纯函数

不同于 Elm,ReasonML 并非考虑做为纯函数式语言,也没有“永远不出现运行时错误”这一目标。这意味着 ReasonML 非常务实,聚焦于开发效率和尽快产出。

👍 👍 类型系统

ReasonML 本质上是 OCaml,其类型系统可与 Haskell 媲美。最大缺点是缺少类型类(Typeclass),但是支持高阶模块函子(functor)。

ReasonML 是静态类型的,其类型推断和 Haskell 一样优秀。

👍 👍 生态系统

和 TypeScript 一样,ReasonML 可使用整个 JavaScript 生态系统。

👍 与 JavaScript/TypeScript 的互操作

ReasonML 编译为 JavaScript,因此可在同一项目中同时使用 JavaScript、TypeScript 和 ReasonML。

👍 ReasonML 和 React:天作之合

前端 Web 开发人员常使用 React,但是很少有人知道 React 最初是用 OCaml 编写的,近期为了扩大使用才移植到 JavaScript。

由于 ReasonML 是静态类型的,因此无需操心 PropType。

回顾在 JavaScript 一节中给出的例子,看似无害却会导致性能灾难。

代码语言:javascript
复制
<HugeList options=[] />

ReasonML 对不可变数据类型提供很好的支持,代码不会产生性能问题。

代码语言:javascript
复制
<Person person={
    ​id: "0"
    ​firstName: "John"
  ​
  ​friends=[samantha, liz, bobby
  ​onClick={id => Js.log("clicked " ++ id)
/> 

不同于 JavaScript,使用 ReasonML 不会产生不必要的重渲染。获得开箱即可用的良好 React 性能。

👎 工具

ReasonML 的成熟度无法与 TypeScript 等语言相比,并在工具上存在一些问题。例如,其官方推荐的 VSCode 扩展reason-language-server当前无法正常使用,虽然还有其他扩展可用。

ReasonML 本质上使用 OCaml 编译器,OCaml 的编译器错误消息非常难理解。尽管这并非致命,但的确令人沮丧,会影响开发人员的效率。

我期待随着该语言的日渐成熟,在工具上会有所改进。

👍 空值

ReasonML 没有空引用,使用 Option 模式声明可能不存在的值。

👍 不可变性

ReasonML 对不可变数据结构提供一流支持。

👍 模式匹配

ReasonML 提供很好的模式匹配。

评判

可以说 ReasonML 实现了 TypeScript 期望做到但尚未实现的特性。ReasonML 为 JavaScript 添加了静态类型,移除了几乎所有不好的特性,添加了一些的确有用的现代特性。

最佳前端语言奖

ReasonML 获“最佳前端语言奖”。毫无疑问,ReasonML 是前端 Web 开发的最佳选择。

Elixir

Elixir 可能是当前最广为使用的函数式编程语言。和 ReasonML 一样,Elixir 并非一种新语言,而是基于 Erlang 近三十年的成功之上。

Elixir 可称为 Go 的函数式近亲。和 Go 一样,Elixir 从一开始就是针对并发设计的,以利用多核处理器的优点。

不同于其他函数式语言,Elixir 是非常务实的,聚焦于产出。在 Elixir 社区中,不会出现长篇大论的学术讨论。Elixir论坛中满是对现实问题的解决方案干货,社区对新手也十分友好。

语言家族:ML。

👍 👍 生态系统

Elixir 生态系统是一个亮点。对于很多其他语言,是先有了语言,才形成生态系统,二者完全是两码事。在 Elixir,生态系统的核心框架就是 Elixir 团队构建的。Elixir 的创建者 José Valim 同时还是PhoenixEcto这两个 Elixir 生态中最酷软件库的主要贡献者。

大多数其他语言,存在很多解决近乎相同问题的不同软件库,例如多种 Web 服务器、ORM 等。在 Elixir 中,开发工作聚焦于数个核心软件库,给出卓越的软件库质量。

Elixir 的文档非常优秀,其中提供了丰富的例子。不同于其他语言,Elixir 标准软件库同样具有很好的文档。

👍 Phoenix 框架

Phoenix 框架的口号是“Phoenix,感觉好极了!”。不同于其他语言的框架,Phoenix 内建了大量功能,开箱既可用,支持 WebSockets、路由、HTML 模板语言、国际化、JSON 编码解码、无缝 ORM 集成(Ecto)、会话、SPA 工具包等。

Phoenix 框架的性能出奇的好,单机即可处理百万级并发连接

👍 全栈 Elixir

Phoenix 框架近期引入了LiveView,支持在 Elixir 内部构建丰富的实时 Web 接口。想想单页应用的实际场景。无需 JavaScript,也无需 React!

LiveView 甚至可以处理客户和服务器的状态同步。这意味着开发人员不用操心对 REST/GraphQL API 的开发与维护。

👍 数据处理

在很多数据处理任务上,Elixi 完全可替代 Python。对于编写 Web 爬虫任务,Elixir 代码更好,生态更好,完全胜出 Python。

Elixir 可使用Broadway等工具,构建数据接入和数据处理流水线。

👌 类型系统

在我看来,Elixir 的最大缺点是没有适合的静态类型。鉴于 Elixir 并非静态类型,在编译时,编译器和 dialyzer会大量报错。该问题一直存在于 JavaScript、Python 和 Clojure 等动态类型语言中。

👍 速度

Elixir 编译器是多线程的,提供刀锋般迅速的编译速度。相比 JVM,Erlang VM 启动更快,为 Elixir 用户提供很好的运行时性能。

👍👍 可靠性

Elixir 是基于 Erlang 的构建,利用了 Erlang 三十多年构建最可靠软件的经验。运行在 Erlang VM 上的一些程序,可达到99.9999999%的可靠性。没有任何其他平台能达到同样的可靠性。

👍 👍 并发

大多数编程语言在设计上并未考虑并发。这意味着难以编写使用多线程、多处理器内核的代码。这些编程语言使用线程执行并行代码,以及进程读取和写入的共享内存。线程类方法通常易于出错,易于发生死锁,导致复杂性呈指数级增加。

Elixir 构建于 Erlang 之上,具有优秀的并发特性。它采用称为Actor模型的完全不同方法实现并发。使用 Actor,进程(Actor)间不存在任何共享。每个进程维护自己的内部状态,收发消息是进程间的唯一通信方式。

Actor 模型的创建者 Alan Kay最初尝试了面向对象编程,对象间不做任何共享,只是通过消息传递通信。

下面概要比较一下 Elixir 和它的命令式编程表亲 Go。不同于 Go,Elixir 设计上完全考虑了容错。每当 goroutine 崩溃时,整个 Go 程序都会关闭。在 Elixir 中,一个进程死亡时,并不会影响程序的其余部分。更好的是,失败的进程将由其监管进程自动重启,支持失败进程重试失败的操作。

Elixir 进程非常轻量级,开发人员可在单机上轻松生成成百上千的进程。

👍 👍 扩展

再次与 Go 对比。Go 和 Elixir 的并发都使用并发进程间的消息传递。在单机上,Go 程序的首次运行速度更快,因为 Go 编译为原生代码。

一旦扩展到多台机器,Go 程序性能开始下降。原因何在?因为 Elixir 设计完全考虑了在多台机器上运行。Elixir 运行基于 Erlang VM,表现非常出色一旦面对分布式和扩展时。Erlang VM 无缝地承担了大量繁琐事项,包括集群、RPC 功能和联网等。

从某种意义上说,在微服务横空出世之前,Erlang VM 就已经开始实现微服务了。每个进程都可以视为微服务。和微服务一样,进程间彼此独立。在语言内置通信机制的情况下,进程通常可在多台计算机上运行。

避开 Kubernetes 的复杂性而使用微服务。这正是 Elixir 的设计目标。

👍 错误处理

Elixir 采用了一种独特的错误处理机制。Haskell、Elm 等纯函数式语言在设计是考虑错误最小化,而 Elixir 假定错误的发生是不可避免的。

尽管 Elixir 支持抛出异常,但通常并不推荐捕获异常。监管进程会自动重启失败的进程,确保程序的运行。

👌 学习难度

Elixir 是一种简单易学的语言,新手可在一两个月内上手。OTP 是学习中的难点。

OTP 是 Erlang 提供的一组工具和软件库,是一个杀手级特性。Elixir 基于 OTP 构建,是成功地极大简化构建并发和分布式程序的秘方。

尽管 Elixir 本身非常简单,但理解OTP确实需要花费一些功夫。至少对我如此。

👍 学习资源

作为最广为使用的函数式编程语言,Elixir 具有丰富的学习资源。Pragmatic Programmers上提供了数十本很好的 Elixir 数据。这些学习资源大多对初学者非常友好。

👍 模式匹配

Elixir 具有很好的模式匹配支持。

👎 数字密集型运算

Elixir 在计算密集任务上表现不佳。应该选择 Go、Rust 等编译为原生码的语言。

和 Erlang 相比,孰能胜出?

从各方面看,Elixir 和 Erlang 师出同门。Erlang 是一种具有独到语法的强大语言。Elixir 可以认为是一种语法更好的、更先进的,并且具有很好生态系统和社区的 Erlang。

评判

Elixir 可能是所有函数式语言中最强大的,运行在针对函数式编程的虚拟机上,完全针对并发设计,非常适合现代多处理器。

更多信息,可观看Elixir Documentary短片。

奖项

Elixir 荣获两项荣誉。

  • “构建 Web API 最佳语言”奖:归功于其所体系的弹性、函数式优先方法以及很好的生态系统。
  • “构建并发和分布式软件最佳语言”奖:归功于 OTP 和 Actor 模型。不同于命令式编程表亲 Go,Elixir 编写的软件可水平扩展到数千台服务器,提供开箱即可用的容错。

做事选用正确的工具

照片来自由Unsplash照片共享网站的Haupes Co

你会用螺丝刀去钉钉子吗?当然不会。同样,我们也不会使用一种编程语言完成所有的任务。每种语言都有其用武之处。

Go 是系统编程的最佳语言。ReasonML 无疑是前端开发的最佳选择,它满足优秀编程语言的绝大多数要求。Elixir 是 Web API 开发上的绝对赢家,其唯一缺点是缺少静态类型系统,但其具有强大的生态系统、社区、可靠性和并发功能。对于任何类型的并发和分布式软件,最优选择还是 Elixir。

对于数据科学,也许 Python 是不二的选择。

真心希望此文章有所裨益。比较编程语言绝非易事,我尽力而为之。

原文链接: These Modern Programming Languages Will Make You Suffer

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/zCMypl8ZzgT9U60sbZ9x
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券