资讯 人工智能开发者
此为临时链接,仅用于文章预览,将在时失效

什么是好的编程语言?

作者:skura
2019/11/21 10:56

本文作者是高级系统开发工程师 Torbear Gannholm。Torbear 有着 30 多年的开发经验,对技术很深刻的理解。对于什么是好的编程语言,他发表了一篇文章分享了自己的观点。以下是他的全文:

这篇文章改编自我在 Cygni 科技峰会上的一次演讲。

我一直认为编程行为是在一个抽象的领域中完成的,只是后来才被翻译成编程语言。编程应该比写作需要更多的思考。

我基本上仍然坚持这一点,但我在谷歌担任代码可读性审查员的多年经验让我意识到,要想很好地使用一门语言需要时间和经验。许多 C++程序员在使用 Java 时编写了功能完备且合理的面向对象代码,但是 C++的臃肿却不能很好地满足 Java 中的优雅(即使 C++程序员可能认为它除了 Java 的「缺陷」之外大多是优雅的)。

那么程序语言的选择有多重要呢?有没有一种语言可以被认为是完美的?

什么是好的语言?

在学术界,关于什么是一门好语言的话题似乎相当平静,但 Tony Hoare 在 1973 年发表了一个有趣的主题演讲,名为「Hints on Programming Language design」。

如果你不知道 Tony Hoare 是谁,你可能会记得他是几年前在公众场合为发明了「NULL」而道歉的人。他本想避免这个,但实施起来太容易了,所以他无法抗拒。他还发明了很多好东西,比如 switch 语句和通信顺序、进程的思想,它们经常出现在 Go 和 Ada 的并发范例中。他的大部分工作都花在追求无错误编程上,但他断定这是一种浪费,因为业界对此根本不感兴趣。

关于 PL 设计的提示首先提出,一种好的语言应该支持程序员需要完成的工作——主要是设计、文档和调试。因此,编程语言的设计应该引导程序员将程序分解成可管理的部分,帮助程序员使代码大部分具有可读性,当代码发生错误时,不应该太难找到并修复它们。我认为这听起来是一个很好的方法,尽管我还想补充一点,编程在某种程度上也应该很有趣。

随后,Tony Hoare 提出了一些好的编程语言应该具备的特性:

市场可以保持非理性的时间比你保持偿付能力的时间长。

这是经济学家 John Maynard Keynes 的名言。它说明了这样一个观点:我们并不总是认可最好的产品,有可能完美的语言已经被创造出来,但我们并没有使用它。

第一候选语言

task body Controller is

    begin

      loop

         My_Runway.Wait_For_Clear; -- wait until runway is available (blocking call)

         select -- wait for two types of requests (whichever is runnable first)

            when Request_Approach'count = 0 => -- guard - no tasks queuing on Request_Approach

            accept Request_Takeoff (ID: in Airplane_ID; Takeoff: out Runway_Access)

            do -- start of synchronized part

               My_Runway.Assign_Aircraft (ID); -- reserve runway (potentially blocking call if protected 

               Takeoff := My_Runway; -- assign "out" parameter value to tell which runway

            end Request_Takeoff; -- end of the synchronised part

      or

         accept Request_Approach (ID: in Airplane_ID; Approach: out Runway_Access) do

            My_Runway.Assign_Aircraft (ID);

            Approach := My_Runway;

         end Request_Approach;

      or -- terminate if no tasks left who could call

         terminate;

      end select;

   end loop;

end;

上世纪 80 年代,上述观点被大肆宣传,设计师 Jean Ichbiah 认为,十年内,世界上只会有两种编程语言,一种是 Lisp,另一种是 Ada。这不是一个完全不合理的想法。Ada 是在非常坚实的需求集合和与大量团队设计竞争下产生的,这是因为当时计算机语言是一门大生意。Tony Hoare 和 Niklaus Wirth 实际上是另一支半决赛队伍的顾问,获胜的队伍的语言更加复杂,他试图简化他们的意见。Ichbiah 没有想到的是,第一个 Ada 非常复杂,甚至连编译器都有性能问题,所以它有点延迟,然后,第二个编程语言——C 和 Unix 出现了。

但是,由于美国国防部希望创造一种语言来取代他们拥有的 160 种左右的语言,因此在 1991 年 Ada 成为北约系统的强制性语言(尽管例外情况经常被允许)。事实证明,Ada 实际上是一种设计精良的语言,除了军事系统外,它在太空任务、空中交通管制和法国高速列车上也得到了证明。在危急情况下,Ada 应该是最佳选择。对于 C 程序中的每 100 个错误,对应的 Java 程序中大约有 50 个错误,而 Ada 版本中只有 4 个错误。Ada 还有一种方言 Spark,在这里你可以正式证明你的程序的正确性。在生产率方面,Ada 与 Java 差不多。所以,除了复杂性,Ada 似乎非常完美。由于 Ada 在设计上的一致性,所以即使你不知道构造的具体细节,也可以很好地了解代码所做的事情。并且,Ada 有优秀的文档,文档中包含了为什么每个特征会存在。那些用 Ada 编写代码的人似乎也很喜欢这一点。

Ada 继续发展,并在 2012 年获得了合同。不幸的是,似乎不太可能出现 Ada 的复兴。

第二候选语言

-- Type annotation (optional, same for each implementation)

factorial :: (Integral a) => a -> a

-- Using recursion (with the "ifthenelse" expression)

factorial n = if n < 2

                     then 1

                     else n * factorial (n - 1)

-- Using recursion (with pattern matching)

factorial 0 = 1

factorial n = n * factorial (n - 1)

-- Using recursion (with guards)

factorial n

      | n < 2 = 1

      | otherwise = n * factorial (n - 1)

-- Using a list and the "product" function

factorial n = product [1..n]

-- Using fold (implements "product")

factorial n = foldl (*) 1 [1..n]

-- Point-free style

factorial = foldr (*) 1 . enumFromTo 1

似乎每年都有至少一个博主问:今年是否终于是世界恢复理智、Haskell 终于起飞的一年?Haskell 的搜索结果似乎一致称赞这种语言。难道我们使用其它语言的程序员只是不理智吗?

当然,Haskell 是一种非常有趣的语言,但它非常抽象。你真的需要六种不同的方法来实现阶乘吗?我想说,也许 Haskell 的主要关注点在于抽象代数和对数学的兴趣上。有些人确实在生产中使用 Haskell,所以我也看了他们的说法:

那些使用 Haskell 的用户声称对此非常满意,但是一位生产用户说,当他们为了自己私人用途编写代码时,他们更喜欢使用 Python。

除了类型安全性,Haskell 似乎真的没有踩雷,所以我们可能根本没有错过完美的语言。继续向前!

在一门语言中我想要什么样的特性?

为了回答这个问题,我看了一下我使用过的语言,并尝试指定一些我喜欢的特性。如果我设计了一种语言,我会考虑使用以下这些特性。

Cobol

PERFORM LOOP VARYING MyCounter FROM 1 BY 1 UNTIL 10 ...

RECORD

01 namn PIC A(80).

02 personnr PIC 999999-9999.

Cobol 最初是美国国防部管理行政事务的权宜之计。与所有临时解决方案一样,60 年后,它仍在强劲发展,主要是用在银行和政府部门。它稳步发展,最新的版本是 2014 年的。从 for 循环可以看出 Cobol 非常冗长。它考虑的是让非编程业务专家也能够读取代码。虽然我很欣赏这个目标,但我不会模仿这么冗长的内容。另一方面,record 的定义也很棒!你只需指定自己拥有哪些字段以及它们是以何种模式写入的,计算机就会为你处理所有的读写操作。这是一个声明性语法的例子,我特别喜欢模式中的一些可视组件。

FORTRAN

integer, dimension(10, 10, 10)::a

integer, dimension(-5:5)::b

c = a(1, 3:7, 5) + b(3:-1:-1)

我们程序员可能会嘲笑 FORTRAN 是一种过时的语言,但是一旦你进入世界上任何一个物理机构,FORTRAN 很可能就占据了至高无上的地位。事实证明,FORTRAN 与物理学家思考工作的方式非常吻合,而且它还倾向于生成最有效的可执行文件。高效率的一个原因是缺少指针,这使得编译器可以进行更积极的缓存优化。FORTRAN 当然也经过多年的发展,最新的规范是从 2018 年开始的。

另一种在物理系大量使用的语言是 Python,不幸的是它的运行速度非常慢。为了改进这些问题,2009 年开始的一项工作提出了编程语言 Julia,它的目标是像 Python 一样简单,像 FORTRAN 一样快。总的来说,它在这方面很成功,而且它也是一种非常好用的语言。

我从 FORTRAN 中得到的是数组/向量功能。默认情况下,索引开始于 1,但也可以自主定义为从任何地方开始。在上面的代码中,我们看到索引从-5 到 5 的向量 b,向量 c 是一个 5 元素向量,其中元素是 a 和 b 向量切片中相应元素的总和。

其他语言

我使用了很多其他语言,所以在这里快速浏览其中一些语言,也许有些语言有鼓舞人心的效果:

C 语言—更坏也更好

如前所述,编程语言曾经是一门大生意。有人会创建一台计算机,为它设计一个操作系统,然后语言编译器是可选的附加组件。但后来 Unix 出现了,在构建计算机之后,你所要做的就是创建一个相当简单的 C 编译器,并以复制成本获得 Unix 源代码。既然你有 C 编译器,你也可以免费把它装进去。因此 C 语言成为世界上最成功的计算机病毒。

人们会不遗余力地说服自己,这简直是免费的午餐。但仅仅因为 Unix 是用 C 编写的,并不意味着用 C 编写应用程序是件好事。与当时可用的 FORTRAN、Pascal 和其他语言相比,C 语言可能是一个让你想自杀的极好的工具。

从另一方面来说,C 语言是最接近我们可能得到的通用语言的东西,回顾过去,很难想象编程语言、操作系统和许多软件在没有 C 语言的情况下是免费的。

我已经成功地避免了 C++,这是我非常庆幸的。它就像是猪身上的口红,层层叠叠的有着不必要的复杂性。有趣的是,当 GO 被显式地替换为 C++时,结果喜欢 C++的程序员发现,C++ 在复杂程度上很高,这使得他们感觉自己是宇宙的主宰,除了 Haskell 之外,他们永远不会换用任何其他语言。

DSL-解析器/生成器

在 20 世纪 90 年代末,有一些小小的活动来创建特定于领域的语言,因此有一个解析器/生成器是非常有帮助的。我使用了 yacc/lex(和 GNU 等价物 bison/flex)以及 Javacc。我对以前版本的 ANTLR 并不太感兴趣,但是最新的 ANTLR4 非常好,它只是处理你编写规则的方式,其主要思想当然是用描述性声明的方式描述语法。

structureLiteral: LeftBrace (keyValue Comma?)* RightBrace;

keyValue: Key valueProduction;

在某种程度上,我希望它将开始逐渐衰败,将所有内容编码为 XML、YAML 或 JSON,只是为了免费获得解析,并且需要创建更具表现力的语法,所以我肯定认为一种语言应该包含各种类型的解析器/生成器。

SQL-必不可少的恶魔?

with areas as (

       select c.name, count(*) size from closest c

            left join infinites i on c.name = i.name

        where i.name is null

        group by c.name

) select max(size) from areas;

每当我怀着恐惧的心情去写一些 SQL 语句时,我总是拿着一份「SQL for Dummies」,尤其是在有 joins 之类的时髦东西的时候。但是你可以用 SQL 做一些非常强大的事情,只需考虑用一种「普通」的编程语言来做同样的事情。我的灵感来自于 Cygni 的一位同事,他有时使用 SQL 作为应用程序代码,上面的代码来自 aventofcode 2018,在那里我开始使用 SQL 来解决问题。过了一会儿我就放弃了,因为 SQL 不擅长迭代,特别是我使用的 mariadb 版本,但是我也很欣赏 SQL 的优点。

有一些人喜欢批评 SQL,最显著的是「第三个宣言」,其中描述了一系列优秀的数据库语言,称为「D」,这些语言也扩展到了一般编程。「D」的一个版本是「Tutorial D」,它是为教学目的而开发的,目前正在 reldb 中使用。

我认为在语言中有一些关系概念或数据结构是一个好主意,即使在 C 语言中做一些类似 LINQ 的事情。但是,我首先要从 SQL 中获得的是空值处理的性能。

JavaScript-是爱是恨?

let parse = {

 s: function(s) { return [Number(s)]; },

 x: function(s) {

       return s.split("/").map(Number); },

 p: function(s) { return s.split("/"); }

}

let args = parse[m.charAt(0)](m.substr(1));

JavaScript 是许多人讨厌的语言,但也有很多人喜欢它。我喜欢从一个解决方案中以声明的方式创建函数图。但我讨厌当出了问题的时候,我不知道问题在哪里。

Java

class Car {

        int topSpeed() { return 200; }

}


class SportsCar extends Car {

        int topSpeed() { return 350; }

}


Car myCar = new SportsCar();

System.out.println(myCar.topSpeed());

我真的很喜欢 Java。它的效率比 C 高 30%-200%,错误率是 C 的一半。如果我们看一下我们在「PL 设计提示」一开始所设定的标准,我认为它覆盖了它们。

很难选择特定的特性,因为我认为是组合包实现了这一点——许多成功所必需的东西正是很多人喜欢抱怨的。我认为这是伟大的,但对于其他一些语言来说却不是这样。另一个需要考虑的是包的结构。

我认为 Java 的一个错误是它没有简单数据对象的记录或结构类型。

当我说完这番话,总有人问我为什么不提 C#,原因很简单,因为我没有充分使用 C# 语言,不理解它与 Java 的区别(除了它让我恼火的所有方面)。撇开我的偏好不谈,他们似乎有着非常相似的生产率配置文件,而且我不知道有什么足够的客观原因来选择其中一个(除了平台问题,因为反正没有人使用.NET core)。另一方面,视觉语言的效率似乎提高了 30%。

XSLT

<xsl:template match="section[name=’top’]/rule">

        <ul class='{@class}'>

              <li style='list-style: none'>

                     <xsl:apply-templates />

              </li>

       </ul>

</xsl:template>

XSLT 是我一直以来最喜欢的语言,它让我头脑中的各种灯都亮了起来,是引发我对编程语言进行分析的原因。我想用类似 XSLT 的风格来编程,不管这意味着什么。当然,所有的数据在任何时候都应该是 XML 格式的,但是这被整个 XMLSchema 的胡说八道以及用供应商产品替代 freedom 的其他做法扼杀了。

回到 XSLT,看看这种语言的力量。实际上,这里没有太多的代码。match 语句简洁地指出,每当我们遇到「rule」元素,当它是具有值为「top」属性的「section」元素的子元素时,我们应该从这个模板中得到一个结果。现在,你通常会编写多少代码来确定类似的内容?XSLT 几乎没有代码来产生结果,它只是直接写在那里。

我使用 XSLT 的一个「亮点」是我的视角改变了。我不是用命令编写程序,而是用机器运行输入,所以输入实际上是控制输出的程序。

XSLT 的另一个特点是它非常明显的同质化,也就是说,程序本身只是另一个程序可能输出的数据。这是一个有趣的特性,但是如果你真的用它来编写程序,可能会变得很难维护。

Go

rCh := make(chan result)

        for _, n := range numbers {

                go decomp(n, rCh) 

       }

       rs := []result{<-rch code="">

Go 可能与 Haskell 完全相反,因为它缺少语言理论上必须具备的几乎所有特性。它是一种僵硬、枯燥和缺乏想象力的语言,对程序的高效开发非常有帮助。尽管缺乏特征,但对于实践中存在的每一个问题,在 Go 中都有一个优雅实用的解决方案。

Go 是为了更好地适应谷歌开发的语言类型,主要对 C++的复杂度和编译速度的反应。它编译速度非常快,具有垃圾回收功能,并利用 CSP 并发模型允许轻松、安全地使用并发。

在用 Go 编程的同事们表示,他们更喜欢使用 Go,并不再纠结于如何用不同的语言「优雅地」完成任务。

我从 Go 中得到的启发是,不去追求某种理论上的「特征完整性」是可以的。

Dart

void part1(List<Nanobot> bots) {

        var largestRangeBot = bots.reduce((a,b) => a.range > b.range ? a : b);

        bool inRange(Nanobot b) {

              return manhattanDistance(largestRangeBot, b) <= largestRangeBot.range; 

}

var numInRange = bots.where((b) => inRange(b)).length; 

stdout.writeln(numInRange);

}

当使用 V8 引擎的 javascript 开发人员开始考虑如何使程序运行得更快时,他们意识到必须从 javascript 中去掉一些难以加速的垃圾。Dart 看起来并没有什么特别之处,它就好像 Java 和 Javascript 有了一个婴儿,它最终成为一种大家都已经知道的语言。

你为什么要用 Dart?好吧,事实证明,去掉 Javascript 中的垃圾,从 Java 中加入一些好的部分,最终得到了一种语言,这种语言比它的「双亲」工作起来愉快多了,而且效率更高。尽管 web 社区几乎完全放弃了它,但你可以使用 Dart 并将其转换为 Javascript,代码的效率往往比任何人手工编写的代码都要高。它在谷歌内部被大量使用,因此不会有陷入困境的风险。还有一个杀手级的移动开发环境叫做 Flutter。

到目前为止,我的主要收获是提供一个大型且功能强大的标准库的「包括电池」政策。

使用哪种线程安全模型?

每一种现代编程语言都需要一种处理并发性的好方法,我们不能寄托于运气了。那该选哪一种呢?

什么是好的编程语言?

我不知道我会选哪一个,它们都有各自的优势。目前我倾向于将不可变性和事务性内存结合起来。

我卑微的尝试,Tailspin

现在我已经考虑这个问题 15 年了,我想是时候尝试创造一种语言了,希望它足够有趣。下面是一些代码示例。

首先是 FizzBuzz 的实现:

templates fizz

 $ mod 3 -> #

 <0> 'Fizz' !

end fizz

templates buzz

 $ mod 5 -> #

 <0> 'Buzz' !

end buzz

[ 1..100 -> '$->fizz;$->buzz;' ] -> [i](<''> $i ! <> $ !)... -> '$;

' -> !OUT::write

我们首先定义真正的函数,但我决定改掉名字,以避免陷入先入为主的概念。因此,我们定义了一个名为「fizz」的模板部分,它简单地获取输入模 3 并发送它进行匹配。如果它是零,它输出字符串「Fizz」,否则什么也不会发生。我们对「buzz」也一样。

在最后一行中,我们首先创建一个列表/数组,其内容是通过将整数的流/范围从 1 到 100 转换为一个字符串来生成的,其中第一部分是 fizz 模板的输出,第二部分是 buzz 模板的输出。然后将整个数组发送到提供索引 i 的数组模板中,在这里我们匹配每个元素。如果是空字符串,则输出索引,否则输出字符串。然后,我们将所有数组元素流式输出,并将它们转换为一个字符串,在最后加上一个换行符,然后将其发送到 stdout。请注意,「$」在每个转换步骤的含义都会更改为表示进入该步骤的当前值。

接下来,我们有一个小程序将单词放在一行上,并按相反的顺序打印出来:

composer words

 [ <word>* ]

rule word: <~WS> <ws>?

end words


$IN::lines -> '$ -> words -> $(-1..1:-1)...;

' -> !OUT::write

这里我们有一个不同类型的函数,一个 composer,它接受一个 unicode 字符流并将它们解析到第一行的产品中,一个「word」产品数组。

「word」按规则生成一个连续的非空白字符元素,后跟一个可选的连续空白字符元素。如果我们想忽略/丢弃空白,我们可以将该产品放在括号中,比如「(<ws>?)」,但是反过来,我们还是希望单词之间有空格,所以为什么不保留它呢?

在最后一行中,我们从 stdin 读取一系列行,并为每个行创建一个以 new line 结尾的新字符串,其中的内容是解析为数组的原始行,然后将其反转并流式输出。然后打印字符串。

最后一个例子是计算第 n 个斐波那契数的模板部分:

templates nthFibonacci

{ N: $, n0: 0, n1: 1 } -> #

<{ N: <0> }>

 $.n0 !

<{ N: <1..>}>

  { N: $.N - 1, n0: $.n1, n1: $.n0 + $.n1} -> #

<{ N: <..-1>}>

{ N: $.N + 1, n0: $.n1 - $.n0, n1: $.n0} -> #

end nthFibonacci


8 -> nthFibonacci -> !OUT::write

在模板中,我们首先创建一个表示当前状态的对象,因此 N 是输入,n0 和 n1 是斐波那契函数的种子。此对象被发送到匹配器。

如果 N 为零,我们的工作就完成了,n0 是我们正在寻找的值。

如果 N 是 1 或更大,我们创建一个新的状态对象,其中 N 减少,斐波那契关系向前一步计算。然后这个新对象被发送回匹配器。

如果 N 是负的,我们增加 N 并反向执行斐波那契步骤,然后发送给匹配器。

这是完美的语言吗?我不知道,但它当然不止如此,但到目前为止,我真的很高兴使用它进行编码和开发。如果您感兴趣,可以查看它的 github

via:https://cygni.se/the-perfect-programming-language/ 

雷锋网雷锋网雷锋网

长按图片保存图片,分享给好友或朋友圈

什么是好的编程语言?

扫码查看文章

正在生成分享图...

取消
相关文章