Erlang logo

学术和历史问题

封面

展开全部
收起全部

目录

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% 的情况下耗时少于 20 毫秒”的统计保证。 软实时系统(例如 Erlang)可让您做出这种保证。

一条经验法则是,直接编写可以在几毫秒内响应外部事件的 Erlang 程序。 Erlang 中对此有帮助的部分是

  • 那些使程序员难以粗略估计性能的语言功能从未添加到 Erlang 中。 例如,Erlang 没有惰性求值。

  • Erlang 进程非常轻量级,比操作系统线程轻得多。 在 Erlang 进程之间切换通常比在操作系统线程之间切换快一到两个数量级。

  • 每个 Erlang 进程都是单独进行垃圾回收的。 这避免了如果所有进程都有一个大的堆时会出现的(相对较长)延迟。

10.6  第一个 Erlang 编译器是如何编写的?

(或者:Erlang 是如何自举的?)用乔的话来说

首先,我设计了一个执行 Erlang 的抽象机器。 这被称为 JAM 机器; JAM = Joe 的抽象机器。

然后我编写了一个从 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 至少 为你提供以下级别的“保证”

超级安全

接收者在处理后发送确认; 发送者链接,发送,等待 ackEXIT。 这意味着发送者知道,对于每条消息,它是否被完全处理。

中等安全

接收者不发送确认; 发送者链接,发送消息。 这意味着 EXIT 信号会通知发送者,某些消息可能从未被处理。

相当安全

接收者不发送确认; 发送者发送消息。 :-)

这些组合有很多(例如,接收者不是在每条消息之后发送确认,而是在处理中的某些关键点发送)。

Per 的结论是,“如果你认为 TCP 保证传递,而大多数人可能确实这样认为,那么 Erlang 也是如此”。

10.10  Erlang 有哪些限制?

Erlang 语言没有指定任何限制,但不同的实现对进程数、最大 RAM 容量等有不同的限制。这些限制在每个实现的文档中都有记录

10.11  在代码更改期间,函数(funs)的行为如何?

是也不是。函数(fun)是对代码的引用;它不是代码本身。因此,只有当它引用的代码实际加载到评估节点上时,才能评估函数(fun)。在某些情况下,我们可以确定执行永远不会返回到函数(fun)。在这些情况下,可以毫无问题地清除旧代码。以下代码不会导致任何意外

	Parent = self(),
	F = fun() -> loop(Parent) end,
	spawn(F).
	

另一方面,绑定函数(bound 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  导致意外的绑定函数(bound funs)的示例

一些典型的陷入困境的方法是

  • Mnesia。如果将函数(fun)存储在 mnesia 中,则需要在加载代码时更新表(即替换函数引用)。

  • 序列化。同样的事情。如果序列化一个函数(fun)(例如 term_to_binary/1),然后反序列化它,则只有当它引用的代码仍然存在时,才能评估该函数(fun)。一般来说,序列化一个函数(fun),将其保存到文件中,然后在另一个模拟器中加载它,不会达到你期望的效果。

  • 消息传递。将函数(fun)发送到远程节点总是可以的,但该函数(fun)只能在具有相同模块的相同版本的节点上进行评估。

避免函数(fun)引用不存在的代码的问题的通用方法是按名称而不是按引用存储函数,例如,通过编写 fun M:F/A

10.13  只有我一个人觉得记录(records)很丑陋吗?

与 Erlang 的其余部分相比,记录(records)相当丑陋且容易出错。它们之所以丑陋,是因为它们需要大量的输入(没有双关的意思)。它们之所以容易出错,是因为定义记录(records)的常用方法 -include 指令,无法防止记录(records)的多个不兼容定义。

已经探索了几种前进的方法。一种是类似于 Lisp 的 结构体(structs),这已经在邮件列表中讨论过。 另一种是 Richard O'Keefe 的 抽象模式(abstract patterns),这是一个Erlang 增强提案。还有人建议使记录(records)更可靠

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 进程视为对象,那么五个特性中的三个是明确存在的——例如,“开放递归”只是向 self() 发送消息。另外两个,子类型多态性和继承,则不太容易自然地建模。

还有另一种方法是提高消息传递在 OO 中的重要性。在这样的讨论中,经常会提到 Alan Kay,原因有三:他被认为是创造“面向对象”这个术语的人,消息传递是他解释“面向对象”含义时首先提到的东西(例如,在这封电子邮件中),并且他没有指定关于 继承 如何工作的严格解释。

许多人发表了关于“如何在 Erlang 中进行 OO”的论文,第一本关于 Erlang 的书(现已绝版)有一个完整的章节讨论这个问题。在实践中,很少有真正的 Erlang 系统尝试遵循这些“OO Erlang”约定。WOOPER 项目是一个严肃的例子,它试图在 Erlang 之上放置一个 OO 层。

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 内其他操作系统线程上的其他 Erlang 进程会继续运行。 进程停止的时间通常很短,因为一个进程的堆大小通常相对较小;远小于所有进程堆的组合大小。

新启动的 Erlang 进程的 GC 策略是完全扫描。 一旦进程的活动数据增长到超过一定大小,GC 就会切换到分代策略。 如果分代策略回收的量少于一定量,则 GC 将恢复为完全扫描。 如果完全扫描也无法恢复足够的空间,则会增加堆大小。

10.20  我可以利用有关程序的知识来提高 GC 的效率吗?

也许可以,但您最好将精力花在思考其他方法来加快系统运行速度上。

spawn/4 的一个版本接受选项列表作为最后一个参数。 Ulf Wiger (AXD301) 说,如果您进行一些测量、基准测试和思考,可以使用 gc_switchmin_heap_size 来获得更好的性能。 当您可以通过将其初始堆设置得足够大,以避免在进程的生命周期中进行所有 GC 时,可以在短生命周期的进程中完全避免 GC。

当您知道某个进程的堆将快速增长到远高于系统的默认大小时,min_heap_size 会很有用。 在这种情况下,使用当前的 GC 实现,您会获得特别差的 GC 性能。

gc_switch 会影响垃圾收集器从完全扫描算法更改为分代算法的时间点。 同样,在不包含太多二进制文件的快速增长的堆中,使用更高的阈值,GC 可能会表现更好。

10.21  Java 虚拟机可以用来运行 Erlang 吗?

是的。

Erjang 正是这样做的。 这是一个实验性系统,取得了一些令人瞩目的成果。

10.22  是否可以证明 Erlang 程序的正确性?

函数式编程语言的一个被吹捧的优点是,更容易正式地推理程序并证明给定程序的某些属性。

用于执行此操作的两个工具是 MCErlangConcuerror

10.23  我听说在 Erlang 中进行防御性编程不是一个好主意。 为什么?

Erlang 编码指南 建议避免防御性编程。 “防御性编程”一词的选择是不幸的,因为它通常与良好的实践相关联。 该建议的重点是,当 Erlang 程序内部出现错误时,允许 Erlang 进程退出是一种好的方法,也就是说,编写试图避免退出的代码通常不是一个好主意。

例如,在解析整数时,只需编写

	I = list_to_integer(L)
	

如果 L 不是整数,则进程将退出,并且某个位置的 supervisor 将重新启动系统的该部分,并报告错误

	=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 的 原始解释 在线提供。