首页 > 新闻 > 正文

神译局是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能够被称作函数式编程的“语言之母”。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

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

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,它的设计者不得不做出许多取舍。就如俄罗斯一句谚语所说,“同时追赶两只兔子的人最终只会一无所获”。

延伸阅读:

译者:俊一

猜你喜欢
文章评论已关闭!
picture loss