如果函数式编程语言真的很简洁,为什么它们在语言 shootout 游戏中排名不太好呢?

23

我在语言 shootout 游戏中比较了各种编程语言的代码大小。这里是我得到的结果摘要(按长度排序,相似分数分组)。

  1. Python、Ruby、JavaScript、Perl、Lua、PHP、Mozart/OZ
  2. OCaml、Erlang、Racket、Go、Scala、F#、Smalltalk
  3. Pascal、Clean、Haskell、Common Lisp、C#、Java、C
  4. C++、Ada、ATS

我想知道为什么。赢家似乎都是普通的动态语言。Erlang、Racket(原名 PLT Scheme)和 F# 表现不错。Haskell 和 Common Lisp 看起来不比声称冗长的 Java 更简洁。

更新:

我发现了一篇关于此主题的有见地的文章,其中有图表。我还发现了一个针对更大型程序(一个简单的光线追踪器)的类似语言比较。总体而言,我不能说我得到了“完全”的答案,但我得到了一些值得思考的东西。


3
您能否对射击游戏的情况进行更详细的解释?您提供的链接网站让人感到困惑。 - David Thornley
9
你认为每个示例都是由该特定语言的专家编写的吗?从F#代码来看,我发现这只是从C#直接移植到F#,而C#本身则是从Java移植而来。几乎不是典型的函数式代码。由于代码似乎主要由对C++库的P/Invoke调用组成,我甚至不确定这个基准测试应该衡量什么 - 我猜测这是关于Mono的P/Invoke速度,因为显然基准测试甚至没有在MS运行时上运行... - Joel Mueller
2
我认为Haskell表现不佳的原因是,一旦你开始优化它的速度,Haskell代码会膨胀得相当厉害。 - sepp2k
2
@igouy - 这个问题最初是关于简洁的函数式语言的讨论。请告诉我一个衡量 F# 与 GMP 库互操作性的基准测试,这对 F# 语言的简洁性有何启示。我的观点仅仅是我找到的第一个熟悉的函数式语言的源代码并不特别具有函数式的本质——这就质疑了使用这个特定基准测试作为衡量函数式语言简洁性的标准的想法。 - Joel Mueller
1
@igouy - 很好,它衡量了F#与非托管代码交互的简洁性。感谢您强调了我的更大观点,即一个基准测试,其中许多参赛者主要包括对外部库的调用,可能不是进行语言冗长度调查的最佳起点。 - Joel Mueller
显示剩余17条评论
8个回答

24

如果函数式语言确实很简洁...

1 - 大规模编程与小规模编程不同

不能把微小的基准测试程序作为每种语言提供的抽象和模块化如何适用于大规模编程的例子。

2 - 大部分在基准测试中看到的内容只涉及每种语言实现中贡献最快的程序(通常会删除较慢的程序 - 删除哪些程序以及何时删除是任意的)。

{编辑:Adam,既然你不愿相信摘要页面只参考最快的程序的话 - 看一下筛选数据行的脚本,“哪种编程语言最好?” 页面。查看 lib_scorecard.php 中 ValidRowsAndMins 函数的第80行和第82行 - Alioth发布了他们自己的安全证书,因此您的浏览器会有所反应。}

因此,以 Haskell 为例,您正在查看贡献最快的 Haskell 程序的代码大小。

3 - 没有删除任何 meteor-contest 程序,meteor-contest 是无限制的编程比赛 - 最小的 meteor-contest Haskell 程序是最慢的 meteor-contest Haskell 程序


6
+1 是因为我做不到 +10。枪战并没有在任何有用的意义上测试简明性。10行和20行并不重要,重要的是10000行和20000行,或者10000000行和20000000行。哦,还有理解那些1000万行。 - Gilles 'SO- stop being evil'
1 - 那个维基百科链接并没有证实你的说法。如果一种语言有良好的抽象方式,它应该有一个更简单易用的API。 2 - 我在网站上找不到任何关于他们偏向更快程序的参考资料。 - Adam Schmideg
@Adam Schmideg 1 维基百科解释了微型程序和大型程序之间的基本区别。这些微型程序不提供创建或使用抽象的机会,而大型程序则提供了这种机会。2 没有偏见,一切都是关于更快的程序!帮助页面“我在哪里可以看到以前的程序?”http://shootout.alioth.debian.org/help.php#previous - igouy

19
  1. 没有一种语言总是优于另一种语言(好吧,有几个例外... ; )),因此同样适用于一组广泛分类的语言。基准涵盖了广泛的主题,对于某些主题,X可能不如Y合适。
  2. 源代码已经被压缩,我们并不知道程序有多少行以及长度是多少(是的,这是一个指标)。
  3. 相当数量的函数式编程语言仍然比普遍使用的命令式,静态类型语言更好 - 函数式编程语言并不是不简洁,但动态语言允许使用更简洁的程序。
  4. 至少在Haskell中,更简洁的潜力来自于您可以自己构建的抽象 - 但您确实必须自己构建它们并将它们包含在解决方案中。聪明的Haskell黑客可能会用20行实现一个单子,从而使小问题的解决变为20行而不是30行 - 这种抽象对于小程序来说并不划算,但对于较大的程序(例如200行而不是300行)可以节省很多行。我想Lisp也适用(只是宏而不是单子)。
  5. 不要过分认真对待粉丝。FP很棒,值得研究,但它不能治愈癌症,也不能神奇地将任何代码缩短25%。
  6. 对于某些领域,它们仍然可以击败动态语言:例如,由于代数数据类型和模式匹配,在许多函数式语言中,树状数据结构及其处理非常自然。

程序的长度(是的,这是一个指标)是格式风格的指示器 - 例如,Ada 程序似乎长而窄,而 Haskell 程序则似乎更短但更宽。 - igouy
@delnan >> 50行,每行50个字符比100行,每行25个字符稍微更易读(其他条件相同)<< 你明白为什么报纸和杂志要印在窄栏中吗? - igouy
@David Liu - 我希望看到我们的观点得到支持,展示在每种情况下我们的阅读和代码编辑有多有效;-) - igouy
1
是的,没有人提到J语言用于科学计算时,代码大小几乎无法匹敌! - Sanjay Manohar
1
压缩代码将极大地抑制重复,例如冗长的关键字密集习语。 压缩后的代码绝不是源码大小的准确指标。 - zxq9
显示剩余6条评论

10

这似乎是一个展示的机会:

有哪些统计研究表明Python更“高效”?

重点是,原来的问题试图使用一些(微薄、不合适的)数据对编程语言之间的比较做出概括性的结论。但事实上,使用任何数据对编程语言进行合理的量化比较几乎是不可能的。

然而,以下思考值得我们深思:

  • 所有条件相等的情况下,动态类型语言可能更加简洁,因为它们不需要花费时间来描述数据类型
  • 所有条件相等的情况下,在静态类型语言中,类型推断语言可能更加简洁,因为它们不需要在各个位置声明类型
  • 所有条件相等的情况下,在静态类型语言中,具有泛型/模板的语言更可能是简洁的,因为没有此类功能的语言需要反复编写代码或进行转换和间接引用操作
  • 所有条件相等的情况下,具有简洁lambda语法的语言可能更加简洁,因为lambda可能是编程中最重要的抽象之一,用于避免重复和样板代码

话虽如此,情况千差万别。


2
阿里奥斯游戏中的程序并不能真正代表这些语言中的程序。首先,在那里实现的代码高度优化,针对Shootout特定基础设施,这可能导致在功能性语言中使用不太惯用且更加臃肿的代码。这就像一些Ruby库将性能关键代码编写为C一样 - 查看该C代码并宣称Ruby笨重且低级将真的不能公平地评价这种语言。

其次,函数式语言被吹捧为非常简洁的一个重要原因是它们擅长进行抽象。这在大型程序中比在单个功能中更有帮助,所以专门为易于简洁而设计的语言在这方面获胜。Python,Perl和Ruby专门设计为使短程序变得简短,而大多数函数式语言具有其他目标(虽然这并不意味着它们会忽略代码大小)。

有哪个程序能够“真正代表这些语言中的程序总体”?- 您的应用程序是最终基准 http://shootout.alioth.debian.org/flawed-benchmarks.php#app - igouy

2

我最近将一个相对简短的Java程序移植到了OCaml。我之前涉猎过SML和Haskell,也有广泛的C语言经验。

在这个答案中,我讨论了命令式代码与纯函数式代码(即没有变异)的比较。如果允许命令式代码潜入纯函数式程序中,那么你在比较什么?

根据我的经验,纯函数式编程(PFP)是优雅的,但不比命令式编程更简洁。在PFP中,减少了“声明”样板文件,但增加了“转换”样板文件;例如,从元组中解包、尾递归辅助函数等。因此,两种范例都不允许纯粹的程序“主体”得到无阻碍的表达。

PFP的开销较低,编写一个程序以演示给定算法的原理在PFP中很好地进行。但是,在使其“真实世界化”,处理错误条件和非法输入并打印诊断方面,它增加了很多膨胀,而在命令式语言中则更容易实现。


确实。这就是为什么大多数人更喜欢不纯的函数式语言。图算法是一个很好的案例研究,其中纯解决方案往往与它们的命令式对应物相比看起来和表现得非常糟糕。 - J D

1

必须与大多数编程语言中可用的庞大面向对象编程库有关,以及像反引号用于shell调用和Perl正则表达式语法这样的老掉牙的技巧。参考Python

pw = file("/etc/passwd")
for l in pw:
    print l.split(':')[0]

如果没有面向对象的语言中充斥着的抽象概念,要在系统上打印所有用户名将需要更多的代码。我并不是说其他编程范式无法做到这一点,但趋势是每种类型都有许多成员函数,使得繁琐的任务变得简单。个人而言,我认为纯函数式编程语言仅适用于学术目的(但话又说回来,我又算是个什么呢)。


这可能解释了为什么动态语言得分更高。但它并没有回答为什么函数式语言得分如此之低的问题。 - Adam Schmideg
4
这并没有解释任何事情。如果函数式语言有丰富的库,我们可以编写类似于 file("/etc/passwd") map l.split(':')[0] 的代码。 - Andrey
1
函数式编程语言更偏向于学术界,它们没有广泛的库社区,这意味着你必须自己多做一些工作。而且,由于其面向学术界,函数式编程语言解决的问题完全不同(并且比如动态语言更加简洁地解决问题)。 - You
6
在Haskell中,它是readFile "/etc/passwd" >>= mapM (putStrLn . takeWhile (/=':')) . lines,比Python更长7个字符(如果在Python和Haskell示例中都尽量减少空格,则更少3个字符)。其中四个字符是因为在Haskell中它被称为readFile,而在Python中它被称为file - sepp2k
你所描述的任务是一个系统脚本任务,这正是像Python这样的语言最初设计的目的。因此,它在这方面表现出色并不令人意外。而函数式语言在其他任务(如操作具有复杂结构的数据)方面会更加出色。 - Gilles 'SO- stop being evil'
在 F# 中:for l in System.IO.File.ReadAllLines "/etc/passwd" do l.Split(':').[0] |> printfn "%s" - J D

1

你应该考虑到,你的第一组语言(脚本)比C/C++慢30到100倍,对于函数式语言,这个差距在2到7倍之间。列表中的程序都经过了速度优化,而测量其他方面只是次要问题,这并不能真正反映语言的实际状态。


更有趣的是看表格,其中代码大小和运行时间各自权重为1。这样你就可以得到一个速度/可维护性比率的比较,这似乎是一个比仅仅考虑代码大小更好的指标。


-1
获胜者似乎是普通的动态语言。
Lisp是一个明显的反例,它是一种非常冗长的普通动态语言。另一方面,APL/J/K可能比其他任何语言都更简洁,并且它们是动态的。还有Mathematica...
Haskell和Common Lisp看起来并不比声称冗长的Java更简洁。
你的数据是针对已经针对性能进行了优化的小程序,并且使用特定设置的GZIP算法压缩后的代码大小作为度量标准,因此你不能仅凭这些数据得出一般性结论。也许更有效的结论是你正在观察到由性能优化导致的膨胀,因此从你的数据中最简洁的语言是那些无法被优化因为它们本质上是低效的(Python、Ruby、Javascript、Perl、Lua、PHP)。相反,Haskell可以通过足够的努力进行优化,以创建快速但冗长的程序。这真的是Haskell与Python相比的劣势吗?另一个同样有效的结论是,Python、Ruby、Perl、Lua和PHP在使用该设置的GZIP算法进行压缩时具有更好的效果。也许如果你使用游程编码或算术编码或LZ77/8,也许带有BWT预处理或另一种算法重复实验,你会得到完全不同的结果?

该网站的代码中还有大量无用的垃圾。看看这段OCaml代码,只有在您的OCaml安装已经过时两代时才是必需的:

(* This module is a workaround for a bug in the Str library from the Ocaml
 * distribution used in the Computer Language Benchmarks Game. It can be removed
 * altogether when using OCaml 3.11 *)
module Str =
struct
  include Str

  let substitute_first expr repl_fun text =
    try
      let pos = Str.search_forward expr text 0 in
      String.concat "" [Str.string_before text pos;
                        repl_fun text;
                        Str.string_after text (Str.match_end())]
    with Not_found ->
      text

  let opt_search_forward re s pos =
    try Some(Str.search_forward re s pos) with Not_found -> None

  let global_substitute expr repl_fun text =
    let rec replace accu start last_was_empty =
      let startpos = if last_was_empty then start + 1 else start in
      if startpos > String.length text then
        Str.string_after text start :: accu
      else
        match opt_search_forward expr text startpos with
        | None ->
            Str.string_after text start :: accu
        | Some pos ->
            let end_pos = Str.match_end() in
            let repl_text = repl_fun text in
            replace (repl_text :: String.sub text start (pos-start) :: accu)
                    end_pos (end_pos = pos)
    in
      String.concat "" (List.rev (replace [] 0 false))

  let global_replace expr repl text =
    global_substitute expr (Str.replace_matched repl) text
  and replace_first expr repl text =
    substitute_first expr (Str.replace_matched repl) text
end

单核版本通常包含大量并行代码,例如OCaml中的regex-dna。看看OCaml中的fasta:整个程序被复制了两次,并且根据字长进行切换!我这里有一个旧版的OCaml fasta程序,大小不到那个程序的五分之一...

最后,我应该指出,我曾经为这个网站贡献过代码,但因为它太好了而被拒绝。撇开政治不谈,OCaml二叉树曾经包含“由Isaac Gouy取消优化”的声明(尽管已经删除了评论,但取消优化仍然存在,使OCaml代码变得更长和更慢),因此您可以假设所有结果都经过了主观篡改,特别是引入了偏见。

基本上,使用这样质量低劣的数据,您无法希望得出任何有见地的结论。您最好尝试找到更重要的已在不同编程语言之间移植的程序,但即使如此,您的结果也将是特定于领域的。我建议完全忘记 shootout...

当使用OCaml 3.11时,它可以完全被移除。遗憾的是,没有OCaml支持者贡献最新的代码 - Objective Caml本地代码编译器,版本3.11.1。 - igouy
3
只是因为它没有达到所需的要求而被拒绝。 - igouy
@iguoy: "可惜没有OCaml党派贡献最新的代码。" 你需要一个OCaml党派为你从网站中删除那个代码块吗? - J D
3
当一个OCaml专家注意到他们可以对OCaml程序进行简单的改进时,你认为他们会继续贡献一个经过测试的具有这种变化的程序,还是花费5分钟在公共论坛上抱怨代码?想一想。 - igouy
2
我认为最好的是那些自称为OCaml程序员的人来贡献OCaml程序,而不是那些不自称为OCaml程序员的人。 - igouy
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接