神译局是36氪旗下编译团队,关注科技、商业、职场、生活等领域,重点介绍国外的新技术、新观点、新风向。
编者按:编程语言哪种好?这可能是许多学习编程人员甚至是外行人员都会面对的头疼问题。网络上普遍的编程语言介绍,大多都是东拼西凑的内容,并且无法让人真正认识和了解各种语言的优缺点。这篇文章,原标题是These Modern Programming Languages Will Make You Suffer,作者Ilya Suzdalnitski在文章中针对15种编程语言展开了详细测评,希望对你有所帮助。
图片来源:geeksforgeeks
懒人目录
概述篇:编程语言最重要的特征
一星篇:C++,JAVA
二星篇:C#,Python,Rust,TypeScript
三星篇(上):Go,JavaScript
三星篇(下):Haskell,OCaml,Scala
四星篇:Elm,F#
五星篇:ReasonML,Elixir
在继续讨论各种编程语言的排名之前,我想再多说几句题外话。为什么我们要费神进行函数式编程呢?因为它能带给我们安心感。
不可否认的是,函数式编程听上去也许很恐怖。但实际上,也没什么好害怕的。
简单来讲,函数式语言有很多正确的设计。在大多数情景下,函数式语言只有好的特征:给力的带有代数数据类型支持的类型系统,没有空值,没有为错误处理设置异常,内置不可变性数据结构,模式匹配,函数组合操作符。
函数式编程语言到底有哪些普遍优点,以至于其排名如此靠前呢?
和主流的命令式语言不同的是,函数式编程语言提倡纯函数编程。
纯函数是什么呢?这个概念非常简单,即一个纯函数对于相同的输入,会始终返回相同的输出。举个例子,2+2始终返回4,也就是说,加法运算符“+”就是一个纯函数。
纯函数不会直接和外部产生接触,要想使用它们,必须通过API调用,或者从控制台进行写操作。纯函数不允许改变状态,它和OOP采用的方法完全相反,对于OOP,任何方法都能自由地改变其他对象的状态。
辨别纯函数和非纯函数是很简单的事情。如果一个函数没有参数,或者没有返回值,那么它就是一个非纯函数。
这里有一些非纯函数的例子:
// Impure, returns different values on subsequent calls.
// Giveaway: takes no arguments.
Math.random(); // => 0.5456412841544522
Math.random(); // => 0.7542151348966241
Math.random(); // => 0.4534865342354886
let result;
// Impure, mutates outside state (the result variable)
// Giveaway: returns nothing
function append(array, item) {
result = [ ...array, item ];
}
这是一些纯函数:
// Pure, doesn't mutate anything outside the body of the function
function append(array, item) {
return [ ...array, item ];
}
// Pure, always returns the same output given the same input.
function square(x) { return x * x; }
这样的方法似乎局限性很大,我们也需要花很多时间适应,我最开始对纯函数也是很困惑的。
纯函数的优点是什么?它们非常容易测试(不需要stub和mock)。关于纯函数的推论很容易——与OOP不同,函数式编程不需要记住整个应用程序的状态。您只需要专注当前正在处理的函数。
纯函数能够轻易地被组合在一起,它的并发性也很好,因为函数之间不会分享状态。重构纯函数也是纯粹的乐趣——只需要复制和粘贴,不需要复杂的IDE工具。
简单来讲,纯函数能给编程带来许多乐趣。
函数式编程提倡使用纯函数——最好代码的90%都是由纯函数组成。一些编程语言把这一点发展到了极致,完全不允许非纯函数的出现,因为非纯函数并不是一个好的想法。
以下所有的函数式语言都为不可变性数据结构提供了内置支持。数据结构也是持久的,这意味着无论发生了什么改变,我们都不必为整个结构进行深度拷贝。
想象在一个拥有超过100000个元素的数组上完成一次又一次的拷贝,速度一定很慢,对吧?
当增加一些想要的改变之后,持久的数据结构只需要简单地重用旧数据结构的引用,而不需进行拷贝。
代数数据类型(ADT)是一种可以用来对应用状态进行建模的好方法,我们可以把它看作是进阶版的枚举。我们指定类型可以组成的潜在子类型,以及构造函数参数:
type shape =
| Square(int)
| Rectangle(int, int)
| Circle(int);
上面的类型“shape”可以是一个Square,一个Rectangle,或者一个Circle。Square的构造函数带有单个int类型参数来表示宽度,Rectangle带有两个int类型参数,表示宽度和长度,Circle带有单个的int类型参数,表示半径。
下面还有一个简单的Java代码:
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 = radius;
}
}
我不知道你怎么想,但是我肯定会使用前一个版本,也就是说在函数式语言中使用ADT。
所有的函数式语言都可以强有力地支持模式匹配。通常来说,模式匹配允许编写非常有表现力的代码。
这有一个关于option(bool)类型的模式匹配例子:
type optionBool =
| Some(bool)
| None;
let optionBoolToBool = (opt: optionBool) => {
switch (opt) {
| None => false
| Some(true) => true
| Some(false) => false
}
};
同样的代码,如果不使用模式匹配:
let optionBoolToBool = opt => {
if (opt == None) {
false
} else if (opt === Some(true)) {
true
} else {
false
}
}
毫无疑问,模式匹配版本更具有表现力,并且写得很清晰。
模式匹配也可以提供编译时的穷尽保证,也就是说,我们不会忘记去检查任何一种可能的情况。而非函数式语言则不会提供这样的保证。
函数式编程语言通常会避免使用空引用。取而代之的是,它会使用和Rust中类似的Option模式。
let happyBirthday = (user: option(string)) => {
switch (user) {
| Some(person) => "Happy birthday " ++ person.name
| None => "Please login first"
};
};
异常的使用在函数式语言中是不被提倡的。取而代之的是,会使用和Rust中类似的Result模式:
type result('value, 'error) =
| Ok('value)
| Error('error);
let happyBirthday = (user: result(person, string)) => {
switch (user) {
| Ok(person) => "Happy birthday " ++ person.name
| Error(error) => "An error occured: " ++ error
};
};
如果没有管道向右操作符,函数调用会变得嵌套很深,从而使得可读性下降:
let isValid = validateAge(getAge(parseData(person)));
函数式语言有一个特殊的管道操作符,能够使任务变得更加简单:
let isValid =
person
|> parseData
|> getAge
|> validateAge;
Haskell能够被称作函数式编程的“语言之母”。Haskell至今已问世30年了,比Java还久。函数式编程中很多最好的思想就是来自Haskell。
所属的编程语系: ML
没有什么类型系统比Haskell的类型系统还要强大。Haskell支持代数数据类型,也支持类型类。它的类型检查几乎可以推论出一切。
众所周知,如果想要高效地使用Haskell,那就必须先精通范畴论。使用OOP的程序员需要有多年经验,才能写出好的代码,而新手在Haskell前期学习中就需要投入大量时间和精力。
即使是用Haskell写一个简单的“hello world”程序,也需要程序员理解Monad,特别是IO Monad。
根据我的经验,Haskell社区的学术性更高。最近一个关于Haskell库邮件列表的帖子写道:
在一次私信交流中,我了解到,元组函数\ x->(x,x)实际上是对双应用和一些相关结构进行对角化的特殊情况。
这个帖子收到了39个爱好者的回复。
——国际威胁情报、黑客动向以及维基解密资讯平台Hacker News @momentoftop
上述引用就很好地总结了Haskell社区的特点。Haskell社区对学术讨论和范畴论更感兴趣,而不是解决实际问题。
就像我们说过的,纯函数特别强悍,副作用(比如和外界交互,包括转变状态)会导致程序中大量出现错误。
作为一个纯函数语言,Haskell完全不允许使用它们,这意味着函数不能改变任何值,也不允许和外界交互(甚至是log日志在技术上也是不允许的)。
当然,Haskell提供了与外界交互的其他方法。你可能会问它是怎么工作的?我们提供一组指令(IO Monad)。
这种指令可能是:读取键盘的输入,然后在某个函数中使用该输入,然后在控制台上打印出结果。编程语言在运行时读取该指令,并且执行操作。我们不会执行和外界直接交互的代码。
不惜一切代价避免成功!
——Haskell非官方的座右铭
在实际操作中,这样关注函数纯度能够大幅增加抽象数量,这也会增加复杂度,因此会导致开发者效率的下降。
和Rust类似,Haskell没有空引用。Haskell使用Option模式来表示可能不存在的值。
一些函数会抛出错误,惯用的Haskell代码使用Result类型表示(和Rust中类似)。
Haskell为不可变性数据结构提供了一流支持。
Haskell有很好的模式匹配支持。
Haskell的标准库一团糟,特别是默认的prelude(核心库)。默认情况下,Haskell使用抛出异常的函数,而非返回Option值(函数式编程的黄金标准)。更糟糕的是,Haskell有两个包管理器——Cabal和Stack。
硬核的函数式编程不会成为主流——它需要深入理解许多高度抽象概念。
——摘自博客文章《软件设计的4项更佳原则》(Four Better Rules for Software Design),作者David Bryant Copeland
我真的很希望自己能够喜欢Haskell。但遗憾的是,Haskell似乎永远都限制在学术圈子中。Haskell是最差的函数式编程语言吗?看你怎么想的了,反正我认为是。
OCaml是一门函数式编程语言,它是Object Caml的简写。但事实上,你几乎不会发现有任何人在OCaml中使用对象。
OCaml的问世时间几乎和Java相当,名字中的“Object”可能反映了“Object”在那个年代的夸张宣传。OCaml是在Caml基础上进行设计的。
所属的编程语系:ML
OCaml的类型系统几乎和Haskell的一样好,OCaml类型系统最大的缺点就是缺少类型类,不过它支持函子(更高阶的模型)。
OCaml是静态类型的,它的类型推论几乎和Haskell的一样好。
OCaml社区很小,这意味着我们不能找到适用普遍用例的高质量库。举个例子,OCaml缺少良好的web框架。
相比于其他语言,OCaml库的文档特别差。
OCaml的工具很糟糕,它有三个包管理器——Opam,Dune以及Esy。
OCaml以特别差的编译器错误信息而“臭名昭著”,这虽然不是一个致命伤,但也足以令人沮丧了,开发者效率也会受到影响。
关于OCaml的经典书籍是亚伦·明斯基(Yaron Minsky)等作者著作的《真实世界的OCaml》(Real World OCaml)一书。
这本书自从2013年之后就没有再版更新过了,里面的许多例子也都已经过时了。跟着这本书学习不可能跟得上现代工具。
对比其他语言,这门语言的学习教材特别缺乏,大多数都是学术课程的讲义。
“多核随处可见(Multicore is coming Any Day Now™️)”总结了OCaml中并发性的事。OCaml开发者等待恰当的多核支持已经等了很多年了,它在近期似乎还不会加入到OCaml中。OCaml似乎是现在唯一一门缺少恰当多核支持的函数式语言。
OCaml没有空引用,它使用Option模式来表示可能不存在的值。
惯用的OCaml代码使用Result类型模式。
OCaml为不可变性数据结构提供了一流支持。
OCaml有很好的模式匹配支持。
OCaml是一门挺好的函数式语言。它最主要的缺点就是有点劣势的并发性支持,并且OCaml社区很小,因此生态系统也很小,缺乏学习资源。
鉴于这些缺点,我不推荐在开发中使用OCaml。
Scala是少数真正的多重范式编程语言之一,对面向对象编程和函数式编程都有很好的支持。
所属的编程语系:C
Scala在JVM之上运行,这意味着它能获取Java库的大生态系统。这能大幅提高后端开发者的生产力。
Scala可能是唯一一个缺乏恰当类型推论的类型化函数式语言,它的类型系统并不健全。Scala的类型系统不像其他函数式语言那么好。
就好的方面而言,Scala支持高级类类型,以及类型类。
尽管有许多缺点,Scala的类型系统还是很好的,因此我给它点赞。
虽然Scala代码非常简洁,特别是相比于Java而言,但是Scala代码可读性并不高。
Scala是少数属于C语言家族的函数式编程语言之一。C语系的编程语言更多使用命令式编程,而ML语系的编程语言更多是函数式编程。
因此,在Scala中运用类C语法进行函数式编程有时会让人觉得奇怪。
Scala中的代数数据类型没有合适的语法,这会降低可读性:
sealed abstract class Shape extends Product with Serializable
object Shape {
final case class Square(size: Int) extends Shape
final case class Rectangle(width: Int, height: Int) extends Shape
final case class Circle(radius: Int) extends Shape
}
ADT在ReasonML中:
type shape =
| Square(int)
| Rectangle(int, int)
| Circle(int);
由于更好的可读性,ADT在一门ML语言中可以显得更简洁。
由于编译速度,Scala可能是最差的编程语言之一。在更老的硬件上,一个简单的“hello world”程序可能会花上10秒钟来编译。Scala编译器不是并发性的(它使用单核编译代码),这对编译速度没有任何提升。
Scala在JVM之上运行,意味着它的程序会花更长时间启动。
Scala有许多特征,这使得它很难学。和C++一样,Scala的语言特征过多了。
Scala是最难的函数式语言之一(只比Haskell简单一些)。事实上,它极差的学习性是许多公司决定不使用Scala的首要因素。
Scala为不可变性数据结构提供了一流支持(使用样本类)。
从不好的方面而言,Scala支持空引用。从好的方面而言,Scala中处理潜在丢失数值的惯用方法是使用Option模式,就像其他函数式语言一样。
就像其他函数式语言一样,Scala中进行错误处理的惯用方式是使用Result模式。
Scala运行在JVM之上,但JVM不是为并发性建立的。从好的方面而言,Akka工具箱非常成熟,在JVM上提供了类似Erlang的并发性。
Scala有很好的模式匹配支持。
我真的很希望自己能够喜欢Scala,但是我做不到。Scala想要实现太多内容,为了同时支持OOP和FP,它的设计者不得不做出许多取舍。就如俄罗斯一句谚语所说,“同时追赶两只兔子的人最终只会一无所获”。
延伸阅读:
现代编程语言终极测评:概述篇
现代编程语言终极测评:一星篇
现代编程语言终极测评:二星篇
现代编程语言终极测评:三星篇(上)
现代编程语言终极测评:四星篇
现代编程语言终极测评:五星篇
译者:俊一