10 学术和历史问题
10.1 为什么 Erlang 被称为“Erlang”?
Erlang 以数学家 Agner Erlang 的名字命名。除了其他成就之外,他还研究过排队论。最初的实现者也倾向于不否认 Erlang 听起来有点像“ERicsson LANGuage”。
10.2 谁发明了 Erlang?
在 20 世纪 80 年代,爱立信 计算机科学实验室 进行了旨在找出计算机语言的哪些方面使电信系统编程更容易的项目。Erlang 出现在 80 年代后期,是将这些功能整合到一起的结果,这些功能使编写此类系统变得更简单,并避免了那些使它们更复杂或更容易出错的功能。
最初参与的人是 Joe Armstrong、Robert Virding 和 Mike Williams。其他人后来加入,并添加了分布式和 OTP 等功能。
10.3 Erlang 语法来自哪里?
主要来自 Prolog。Erlang 最初是一个修改后的 Prolog。! 作为发送消息运算符来自 CSP。Eripascal 可能负责 , 和 ; 作为分隔符而不是终止符。
10.4 为什么爱立信会赠送 Erlang?
(以下是我的个人印象。我并不代表爱立信!)
没有损失:爱立信的核心业务是电信产品,销售编程工具并不是爱立信真正感兴趣的业务。
刺激采用:Erlang 是一种非常适合许多类型系统的语言。发布一个良好且免费的开发环境可能会让 Erlang 更快地流行起来。
建立良好意愿:赠送很酷的软件只会改善爱立信的形象,尤其是考虑到目前围绕“开源软件”的媒体关注度。
10.5 什么是软实时?
愤世嫉俗的人会说“基本上什么都没有”。
硬实时系统是指能够保证在一定时间内始终执行某个动作的系统。许多简单的嵌入式系统可以做出硬实时保证,例如,可以保证 Z80 CPU 上的特定中断服务例程永远不会超过 34us。对更复杂的系统做出此类保证会越来越难。
许多电信系统有不太严格的要求,例如,它们可能需要类似于“数据库查找在 97% 的情况下需要少于 20ms”的统计保证。软实时系统(例如 Erlang)允许你做出此类保证。
经验法则是,编写能够在几毫秒内响应外部事件的 Erlang 程序非常简单。Erlang 中有助于实现这一点的方面包括
语言特性,使得程序员难以(或更难)粗略估计性能,从未添加到 Erlang 中。例如,Erlang 没有惰性求值。
Erlang 进程非常轻量级,远轻于操作系统线程。在 Erlang 进程之间切换通常比在操作系统线程之间切换快一个或两个数量级。
每个 Erlang 进程都是独立进行垃圾回收的。这避免了(相对)长时间的延迟,因为如果所有进程只有一个大的堆,就会出现这种延迟。
10.6 第一个 Erlang 编译器是如何编写的?
(或:Erlang 是如何引导的?) 用 Joe 的话说
首先,我设计了一个抽象机来执行 Erlang。它被称为 JAM 机;JAM = Joe 的抽象机。
然后,我用 Prolog 从 Erlang 编写了一个到 JAM 的编译器和一个模拟器,看看该机器是否有效。这两个都是用 Prolog 编写的。
同时,Mike Williams 为 JAM 编写了一个 C 模拟器。
然后,我用 Erlang 重写了 erlang-to-jam 编译器,并使用 Prolog 编译器对其进行编译。生成的代码在 C 模拟器中运行。然后,我们抛弃了 Prolog。
其中一些内容在 一篇旧论文 中有所描述
10.7 Erlang 语法和语义细节
如果对语言允许或执行的内容有任何疑问,最好的起点是 Erlang 规范。这仍然是一个正在进行的工作,但它涵盖了语言的大部分内容。
10.8 消息接收的顺序是否得到保证?
是的,但仅限于一个进程内。
如果有一个活动进程,你向它发送了消息 A,然后发送了消息 B,则保证如果消息 B 到达,则消息 A 在其之前到达。
另一方面,假设进程 P、Q 和 R。P 向 Q 发送消息 A,然后向 R 发送消息 B。不能保证 A 在 B 之前到达。(如果需要这样做,分布式 Erlang 会很困难!)
10.9 如果我发送消息,是否保证它会到达接收方?
大多数人发现,最简单的方法是将答案设为“是的,始终”。
Per Hedeland 在邮件列表中介绍了这些问题(略微编辑):
“如果没有任何东西损坏,则保证送达”——如果出现故障,你会发现,前提是你使用了 link/1。也就是说,不仅在链接的进程死亡时,而且在整个远程节点崩溃时,网络中断时,或者在执行链接操作之前发生这些情况时,你都会收到 EXIT 信号。
似乎这个“保证送达”问题不时出现,但我从未弄清楚那些提出这个问题的人到底想要什么。
保证消息被放入接收方的输入队列?但是,如果接收方在从队列中提取消息之前就挂掉了,这个保证就毫无用处,因为消息还是会丢失。
保证接收方从其输入队列中提取消息?嗯,除了显而易见的问题(取决于接收方是如何编写的,即使它一直运行良好,也可能**永远**不会提取该特定消息),它还遭受了之前问题的变体:即使你“知道”接收方已经“消费”了消息,它也可能在以任何方式处理消息之前就挂掉,那么它就相当于从未被发送过。
保证接收方实际**处理**消息?当然是在开玩笑,希望每个人都知道,无论使用什么编程和通信系统,获得这种保证的唯一方法是接收方被编程为在处理完成后发送明确的确认(当然,这可能隐藏在 RPC 等抽象之下,但基本原理仍然成立)。
此外,任何保证都**必须**包含某种来自远程端的确认,至少在分布式系统中是这样,即使它对程序员不可见。例如,你可以让 '!' 块直到来自远程端的确认返回,表明消息已经按你的要求进行了处理——即某种同步通信。但这会惩罚那些**不需要**“保证”并**希望**异步通信的人。
因此,根据你的需求,Erlang 至少为你提供了以下几种“保证”级别。
超级安全
接收方在处理后发送确认;发送方链接、发送、等待ack或EXIT。这意味着发送方知道每条消息是否被完全处理。
中等安全
接收方不发送确认;发送方链接、发送消息。这意味着 EXIT 信号会通知发送方,某些消息可能永远不会被处理。
相当安全
接收方不发送确认;发送方发送消息。 :-)
这些组合有很多种(例如,接收方不是在每条消息之后发送确认,而是在处理过程中的某些关键点发送)。
最后,指出“如果你认为 TCP 保证交付,而大多数人可能都认为是,那么 Erlang 也一样”。
10.10 Erlang 有什么限制?
Erlang 语言没有指定任何限制,但不同的实现对进程数量、最大内存容量等有不同的限制。这些限制在每个实现的文档中都有说明。
10.11 在代码更改期间,fun 的行为如何?
既是又否。fun 是对代码的引用,而不是代码本身。因此,只有当它所引用的代码实际加载到评估节点上时,fun 才能被评估。在某些情况下,我们可以确定执行永远不会返回到 fun。在这些情况下,旧代码可以被安全地清除。以下代码不会出现任何意外情况
Parent = self(), F = fun() -> loop(Parent) end, spawn(F).
另一方面,绑定的 fun 不会被替换。在下面的例子中,即使在代码更改后,F 的旧版本也会被执行
-module(cc). -export([go/0, loop/1]). go() -> F = fun() -> 5 end, spawn(fun() -> loop(F) end). loop(F) -> timer:sleep(1000), F(), cc:loop(F).
这种问题可以在标准行为的 code_change/2 函数中解决。
10.12 绑定 fun 导致意外情况的示例
一些常见的导致问题的方式:
Mnesia。如果你将 fun 存储在 Mnesia 中,你需要在加载代码时更新表格(即替换 fun 引用)。
序列化。也是同样的问题。如果你序列化一个 fun(例如 term_to_binary/1)然后反序列化它,你只能在它所引用的代码仍然存在的情况下评估 fun。一般来说,序列化一个 fun,将其保存到文件,然后在另一个模拟器中加载它,不会达到你期望的效果。
消息传递。将 fun 发送到远程节点始终有效,但 fun 只能在具有相同模块相同版本的节点上进行评估。
避免引用不存在代码的 fun 问题的通用方法是,通过名称而不是引用来存储函数,例如,编写 fun M:F/A。
10.13 是我个人感觉还是记录很丑?
与 Erlang 的其他部分相比,记录相当丑陋,而且容易出错。它们很丑,因为它们需要大量键入(双关语)。它们容易出错,因为定义记录的常用方法,即 -include 指令,没有提供任何针对记录的多个不兼容定义的保护。
已经探索了几种前进方向。一种是 Lisp 式的**结构体**,这在邮件列表中讨论过。另一种是 Richard O'Keefe 的**抽象模式**,这是一个Erlang 增强提案。还有建议使记录更可靠。
10.14 一个进程可以接收具有不同优先级的消息吗?
当然可以。以下是如何使用嵌套接收来实现它的示例
receive -> {priority_msg, Data1} -> priority(Data1) after 0 -> receive {priority_msg, Data1} -> priority(Data1) {normal_msg, Data2} -> normal(Data2) end end.
10.15 Erlang 中的静态类型检查的历史是什么?
Erlang 附带了一个名为 Dialyzer 的静态类型分析系统。使用 Dialyzer 是可选的,尽管许多(甚至大多数)大型项目都使用它。Dialyzer 不需要修改或注释源代码,尽管注释可以增加 Dialyzer 能够发现的问题数量。
在 2005 年之前,静态类型检查很少在商业 Erlang 系统中使用。许多人尝试过各种解决该问题的方案,包括 Sven-Olof Nyström、Joe Armstrong、Philip Wadler、Simon Marlow 和 Thomas Arts。
10.16 Erlang 中的类型检查与 Java/Python/C/C++/Haskell 的类型检查相比如何?
Erlang 本身(即**忽略** Dialyzer)使用动态类型系统。所有类型检查都在运行时进行,编译器在编译时不会检查类型。运行时类型系统无法被绕过。这与 Lisp、Smalltalk、Python、Javascript、Prolog 等语言的类型系统类似。
Java、Eiffel 和其他一些语言的类型系统主要在编译时进行检查,但仍然有一些检查在运行时进行。检查的组合无法被绕过。这种类型系统提供了一些关于类型的保证,编译器可以利用这些保证,这对于优化非常有用。
Haskell、Mercury 和其他一些语言的类型系统完全在编译时进行检查。这种类型系统无法被绕过。这种语言中的类型系统也是一种设计工具,它提高了语言的表达能力。
C、Pascal 和 C++ 的类型系统在编译时进行检查,但可以通过语言提供的简单方法绕过。
10.17 Erlang 与“面向对象”编程的关系是什么?
许多人认为 Erlang 是一种面向并发和消息传递的语言,它恰好是函数式的。即,没有提到面向对象。
另一种常见的观点是,尝试定义“面向对象”,然后看看 Erlang 如何符合该定义。例如,一个相对没有争议的 OO 特征列表是:动态调度、封装、子类型多态性、继承和开放递归。如果你将 Erlang 进程视为对象,那么五个特征中的三个就很明显——例如,“开放递归”就是向自己发送消息。另外两个,子类型多态性和继承,则不太容易建模。
还有另一种方法是提高消息传递在 OO 中的重要性。Alan Kay 经常被提及到这些讨论中,有三个原因:他被认为是“面向对象”这个词的创造者,消息传递是他解释“面向对象”含义时首先提到的内容(例如在这封邮件中),而且他并没有具体说明**继承**应该如何运作。
各种人发表了关于“如何在 Erlang 中进行 OO”的论文,第一本(现已停售)关于 Erlang 的书专门有一章介绍了这个主题。在实践中,很少(如果有的话)真正的 Erlang 系统尝试遵循这些“OO Erlang”约定。 WOOPER 项目是将 OO 层构建在 Erlang 之上的一个严肃努力的例子。
10.18 字符串是如何实现的,如何才能加快我的字符串代码?
字符串表示为链接列表,因此每个字符在 32 位机器上占用 8 个字节的内存,在 64 位机器上占用两倍的内存。访问第 N 个元素是 O(N)。这使得编写 O(N^2) 代码变得很容易。以下是如何发生这种情况的一个例子
slurp(Socket) -> slurp(Socket, ""). slurp(Socket, Acc) -> case gen_tcp:recv(Socket, 1) of {ok, [Byte]} -> slurp(Socket, Acc ++ Byte); _ -> Acc end.
位语法提供了一种替代方法,用于处理与列表实现具有不同空间/时间权衡的字符串。
一些提高字符串相关代码性能的技术
避免 O(N^2) 操作。有时这意味着反向构建一个更长的字符串,然后在最后一步使用 reverse 。其他时候,最好将字符串构建为列表列表,然后让 io 函数展平列表。
考虑使用数据的替代表示,例如,与其将文本表示为字符列表,不如将其表示为单词树。
如果你正在从套接字读取和写入数据,请考虑使用二进制文件而不是字节列表。如果你只是将数据从一个套接字复制到另一个套接字,这将(快得多)。更一般地说,位语法提供了一种替代方法,用于处理与列表实现具有不同空间/时间权衡的字符串。
用 C 语言编写速度关键(首先进行性能分析!)代码,然后将其作为端口驱动程序或链接驱动程序放入你的 Erlang 系统中。
-
端口(和套接字)会为你展平列表,因此自己展平列表会降低速度。这也可以用来创建更可读的代码
gen_tcp:send(Socket, ["GET ", Url, " HTTP/1.0", "\r\n\r\n"]).
10.19 垃圾收集器 (GC) 是如何工作的?
Erlang 中的 GC 在每个 Erlang 进程上独立运行,即每个 Erlang 进程都有自己的堆,并且该堆与其他进程的堆独立进行 GC。
当前的默认 GC 是“停止世界”的世代标记清除收集器。在运行多个线程的 Erlang 系统(在拥有多个内核的系统上的默认设置)上,GC 会停止对正在进行 GC 的 Erlang 进程的工作,但同一个 VM 内其他 OS 线程上的其他 Erlang 进程会继续运行。进程停止的时间通常很短,因为单个进程的堆大小通常相对较小;远小于所有进程堆的总大小。
新启动的 Erlang 进程的 GC 策略是全扫描。一旦进程的活动数据增长到一定大小,GC 就会切换到世代策略。如果世代策略回收的内存少于一定数量,GC 会恢复到全扫描。如果全扫描也无法回收足够的内存,则会增加堆大小。
10.20 我可以利用关于我的程序的知识来提高 GC 的效率吗?
也许可以,但你最好花时间考虑其他方法来提高你的系统的速度。
spawn/4 的一个版本接受一个选项列表作为最后一个参数。Ulf Wiger (AXD301) 说,如果你进行一些测量、基准测试和思考,可以利用 gc_switch 和 min_heap_size 来获得更好的性能。一个“胜利”是,通过设置短生命周期进程的初始堆足够大,以避免在进程的生命周期内进行任何 GC,从而完全避免在短生命周期进程中进行 GC。
min_heap_size 在你**知道**某个进程会快速增长其堆到远高于系统默认大小的情况下很有用。在这种情况下,使用当前的 GC 实现会导致非常糟糕的 GC 性能。
gc_switch 影响垃圾收集器从全扫描算法切换到世代算法的点。同样,在快速增长的堆(不包含很多二进制文件)中,GC**可能**会随着更高阈值的出现而表现得更好。
10.21 Java虚拟机可以用来运行Erlang吗?
可以。
Erjang 就是这么做的。它是一个实验性系统,取得了一些令人印象深刻的结果。
10.22 有可能证明Erlang程序的正确性吗?
函数式编程语言的一个宣传优势是,它更容易对程序进行形式化推理,并证明给定程序的某些属性。
用于实现这一点的两个工具是 MCErlang 和 Concuerror。
10.23 我听说在Erlang中防御性编程是个坏主意。为什么?
Erlang编码指南建议避免防御性编程。术语“防御性编程”的选择是不幸的,因为它通常与良好的实践相关联。该建议的重点是,允许Erlang进程在Erlang程序内部出现错误时退出是**好的**方法,即编写试图避免退出的代码通常是坏主意。
例如,当解析一个整数时,直接写以下代码就很有意义
I = list_to_integer(L)
如果L不是一个整数,进程将退出,某个地方的监督者将重新启动系统的那部分,并报告错误
=ERROR REPORT==== 12-Mar-2003::13:04:08 === Error in process <0.25.0> with exit value: {badarg,[{erlang,list_to_integer,[bla]},{erl_eval,expr,3},{erl_eval,exprs,4},{shell,eval_loop,2}]} ** exited: {badarg,[{erlang,list_to_integer,[bla]}, {erl_eval,expr,3}, {erl_eval,exprs,4}, {shell,eval_loop,2}]} **
如果需要更具描述性的诊断信息,请使用手动退出
uppercase_ascii(C) when C >= $a, C =< $z -> C - ($a - $A); uppercase_ascii(X) -> exit({"uppercase_ascii given non-lowercase argument", X}).
这种错误检测和错误处理的分离是Erlang的关键部分。它通过将正常代码和错误处理代码分开,降低了容错系统中的复杂性。
与大多数建议一样,该建议也有例外。一个例子是输入来自不可信接口的情况,例如用户或外部程序。
Joe 的 原始解释 可在线获取。