Erlang logo

故障排除、问题和“陷阱”

封面

展开全部
收起全部

目录

9 故障排除、问题和“陷阱”

本节介绍许多初学者遇到的问题,从一些小的语法问题开始。如果您遇到了这里没有列出的令人困惑的问题,请尝试将问题发布到 邮件列表

9.1 为什么我不能编写 a = 3(badmatch)?

在 Erlang 中,所有变量都必须以大写字母开头,所以你可以写 A = 3.,但不能写 a = 3.。在后一种情况下,Erlang 会抱怨这是一个“bad match”。

在第二个版本中,'a' 是一个原子,而 3 是一个数字。'a' 不能与 3 **模式匹配**。使用大写字母表示变量来自 Prolog。

如果您不熟悉 **原子** 和 **模式匹配** 这些术语,请阅读 Erlang 书籍 的前一两章。(**非常**粗略地说,原子对应于 C 中的枚举,在这种情况下,模式匹配就像一个 assert 语句。

9.2 为什么我不能更改变量的值?

Erlang 只允许您在一个函数中为变量赋值一次。以下是一些遇到此问题的简单示例

		1> A = 3.
		3
		2> A = 4.
		** exited: {{badmatch,4},[{erl_eval,expr,3}]} **
		3> A = A + 4.
		** exited: {{badmatch,7},[{erl_eval,expr,3}]} **
	

通常的“解决方法”是使用一个新变量

		4> B = A + 4.
		7
	

(这种行为称为“单赋值”,它被认为是一个特性,而不是一个错误。大多数函数式语言,包括 ML 和 Haskell,也具有这种行为。除其他事项外,能够依赖于某个变量始终具有相同的值非常不错。)

9.3 为什么数字列表的打印不正确?

有时 shell(或程序)会以意外的方式打印数字列表,例如

	1> [65, 66, 67].
	"ABC"
	

发生这种情况是因为 Erlang 将字符串表示为整数列表,因此,如果您要求 shell 打印整数列表,shell 会猜测您是想将其作为数字列表还是作为字符串查看。shell 会根据列表是否包含所有可打印字符来进行猜测,因此您可以强制将字符串设置为不可打印

	5> [0, 65, 66, 67].
	[0,65,66,67]
	

在 Erlang/OTP R16B 及更高版本中,可以使用函数 shell:strings/1 在 shell 中关闭此行为

	2> shell:strings(false).
	true
	3> [65, 66, 67].
	[65,66,67]
	

io:fwrite() 也会发生类似的问题,但在这种情况下,您可以通过指定适当的格式化字符来直接控制。 "~s" 始终将参数作为字符串打印, "~w" 始终将其作为列表打印。

9.4 为什么我不能在保护中调用任意函数?

如果允许这样做,就无法保证保护没有副作用。

此外,能够像保护不会消耗任何显着执行时间一样进行编程也很方便。Erlang 书籍和标准 Erlang 规范中列出了可以在保护中调用的 BIF 列表,以下是一些示例:size(),length(),integer(),record()

当使用 if 时,通常会遇到这个问题

	issue_warning() ->
	  if (os:type() == {win32, windows}) ->    %% illegal guard
	    ok = io:fwrite("you are using windows\n");
	  true ->
	    ok = io:fwrite("no problem\n")
	  end.
	

解决方案通常是使用 case。在大多数 Erlang 程序中,case 的使用频率远高于 if

	issue_warning() ->
          case os:type() of
            {win32, windows} -> ok = io:fwrite("you are using windows\n");
            _ -> ok = io:fwrite("no problem\n")
	  end.
	

9.5 为什么我不能在保护中使用“或”?

您可以。从 R6A 开始,用分号隔开的几个保护执行逻辑“或”,例如

	f(N) when (N - 1) > 3; atom(N) -> yes;
	f(N) -> no.
	

对于 f(5)f(blork),它将返回 yes。类似地,逗号用于逻辑与

	g(N) when integer(N), N > 5 -> yes;
	g(N) -> no.
	

注意!分号运算符并不完全与“或”相同,例如 f(hello) 返回 yes ,而评估 f(hello - 1) > 3 or atom(hello) 返回 badarith 错误。第一个示例 f() 的定义与写入以下内容的效果相同

	f(N) when (N - 1) > 3 -> yes;
	f(N) when atom(N) -> yes;
	f(N) -> no.
	

9.6 为什么“catch” 会给我一个语法错误?

写起来似乎很自然

	2> A = catch 1/0.
	** 2: syntax error before: 'catch' **
	

但是 Erlang 的解析规则在这里有点出乎意料,catch 的绑定程度比您预期的要低。为了使其按照您真正想要的方式解析

	3> A = (catch 1/0).
	{'EXIT',{badarith,[{erl_eval,eval_op,3},
                   {erl_eval,expr,3},
                   {erl_eval,exprs,4},
                   {shell,eval_loop,2}]}}
	

9.7 什么是粘性目录?

这通常在使用旧版(上世纪)Erlang 书籍中的示例时出现,例如

        1> c(sets.erl).
        {error,sticky_directory}
	

问题是,已经存在一个名为“sets”的标准库模块。Erlang 运行时系统正在保护您。

最简单的解决方案是重命名您的模块,例如,重命名为 mysets.erl。也可以“取消粘贴”包含库模块的目录。

9.8 为什么我的分布式 Erlang 节点无法通信?

为了使 Erlang 节点能够通信,您需要

  • 节点之间有效的 TCP 网络。在 unix 系统上,您可以使用 telnet 检查这一点,尽管有效的 telnet 不能保证您的网络的足够部分正常工作,例如,DNS 问题会在 Erlang 的分布机制中造成麻烦。

  • 节点必须使用相同的节点命名方案(您不能有一个系统,其中一些节点使用完全限定名,而另一些节点使用短名称)。

  • 节点必须同意使用相同的“神奇安全 cookie”。

以下是如何在名为 **martell** 和 **grolsch** 的两台不同机器上创建两个节点并验证它们是否已连接的示例。在一台机器上

	~ >rlogin martell
	Last login: Sat Feb 5 20:40:52 from super
	~ >erl -sname first_node
	Eshell V4.9.1.1  (abort with ^G)
	(first_node@martell)1> erlang:set_cookie(first_node, nocookie).
	true
	

而在另一台机器上

	~ >rlogin grolsch
	Last login: Thu Feb 3 10:54:20 from :0
	~ >erl -sname second_node
	Eshell V4.9.1.1  (abort with ^G)
	(second_node@grolsch)1> erlang:set_cookie(second_node, nocookie).
	true
	(second_node@grolsch)2> net:ping(first_node@martell).
	pong
	(second_node@grolsch)3> rpc:call(first_node@martell, os, type, []).
	{unix,sunos}
	

pong 告诉我们连接有效,当连接无效时,net:ping() 的结果是 pang rpc:call() 命令说明了在另一个节点上执行命令。

警告

Cookie 是 Erlang 节点的密码。上面的示例将密码设置为“nocookie”,这几乎消除了所有安全性。网络上的任何人都可以使用您的 Erlang 节点来做任何事情,包括删除您所有的文件。

9.9  分布式系统无法工作时

分布式系统无法工作的一个简单原因是,如果您正在组合不同版本的 Erlang。OTP 小组的目标是保持与 Erlang/OTP 的前两个主要版本向后兼容,即 R13B 与所有 R13B 次要版本以及 R12B 和 R11B 兼容。

除此之外,您需要开始深入挖掘。Erlang 节点通过连接到 epmd 守护进程进行通信。守护进程在您第一次启动分布式节点时自动启动。为了调试,您可以通过运行 epmd -kill ) 来杀死它,然后使用调试标志重新启动它。您可以在与 erlang 二进制文件相同的目录中找到 epmd

	~ >/otp/releases/otp_beam_sunos5_r6b/erts-4.9.1/bin/epmd -d -d
	epmd: Sat Feb 5 21:04:39 2000: epmd running - daemon = 0
	epmd: Sat Feb 5 21:04:39 2000: try to initiate listening port 4369
	epmd: Sat Feb 5 21:04:39 2000: starting
	epmd: Sat Feb 5 21:04:39 2000: entering the main select() loop
	

当您在同一系统上启动分布式节点时,您应该在 epmd 窗口中看到一条消息

	epmd: Sat Feb 5 21:07:33 2000: registering 'first_node:1', port 53566
	

同样,当您从另一个系统上的节点使用 net:ping 时,您应该看到一些消息出现。如果 epmd 上似乎没有发生任何事情,则扫描以太网上的数据包以查看信息是否真正通过网络发送出去非常有用。在 unix 系统上,您可以使用 tcpdump 完成此操作。

	# /usr/local/sbin/tcpdump port 4369
	tcpdump: listening on le0
	21:10:17.349286 grolsch.37558 > martell.4369: S 747683789:747683789(0)
	

9.10  避免使用 DNS

使分布式系统无法正常工作的一个相当常见的障碍是 DNS 设置出现故障。避免或测试此问题的一种方法是使用 IP 地址而不是主机名启动分布式 Erlang,例如在一台机器或窗口上

        ~ >erl -name [email protected]
        Erlang (BEAM) emulator version 5.5.5 [source] [64-bit]
        Eshell V5.5.5  (abort with ^G)
	([email protected])1> erlang:set_cookie('[email protected]', nocookie).
	true
	([email protected])2> net:ping('[email protected]').
	pong
        

在另一个窗口中

        ~ >erl -name [email protected]
        Erlang (BEAM) emulator version 5.5.5 [source] [64-bit] [async-threads:0]
        Eshell V5.5.5  (abort with ^G)
        ([email protected])1> erlang:set_cookie('[email protected]', nocookie).
        

9.11  为什么我的应用程序每次加载新代码时都会在第二次死亡?

Erlang 的代码替换系统基于在任何时候加载(最多)两个代码副本,它们被称为“旧”和“新”。当您加载新代码时,当前版本将变为“旧”,而“旧”代码将被丢弃。任何仍在运行“旧”代码的进程都会被杀死。

您可以检查是否有任何特定模块的旧代码仍在运行

	Eshell V4.9.1  (abort with ^G)
	1> l(erl).
	{module,erl}
	2> code:soft_purge(erl).
	false
	

在这种情况下,旧代码仍在运行。您还可以使用 erlang:check_process_code(Pid, Module) 检查特定进程是否正在运行旧代码。

9.12  为什么我不能像普通文件一样打开设备(例如串行端口)?

简短的回答:因为 Erlang 运行时系统不是为此设计的。Erlang 运行时系统的内部文件访问系统 efile 必须避免阻塞,否则整个 Erlang 系统将被阻塞。在软实时系统中,这可不是一件好事。在访问普通文件时,通常可以合理地假设操作不会阻塞。另一方面,设备很可能阻塞。有些设备,如串行端口,可能会无限期地阻塞。有几种可能的解决方法。可以修改 Erlang 运行时系统,或者可以使用外部端口程序来访问设备。可以在 这里 这里 找到关于该主题的两个邮件列表讨论。

9.13  为什么此代码的堆栈回溯没有显示正确的函数?

	-module(erl).
	-export([a/0]).

	a() -> b().
	b() -> c().
	c() -> 3 = 4.           %% will cause badmatch
	

堆栈回溯只显示函数 c(),而不是 a()、b() 和 c()。这是因为进行了尾调用优化;编译器知道它不需要为 a() 或 b() 生成堆栈帧,因为最后执行的操作是调用另一个函数,因此堆栈帧没有出现在堆栈回溯中。

9.14  如果我在编译开源 Erlang 时遇到错误,该怎么办?

考虑使用二进制版本(Debian GNU/Linux 在 woody 和 potato 版本中包含它们,Windows 二进制文件在 下载页面 上)。

在邮件列表中寻求帮助,提供尽可能多的细节,包括

  • 您的系统详细信息(操作系统、操作系统版本、编译器版本)

  • 您遇到的错误消息

  • 您运行 configure 时遇到的任何错误

9.15  为什么 shell 中的错误会杀死其他进程?

因为其他进程与 shell 进程相关联。

每次在 erlang shell 中发生错误时,shell 进程都会退出,并启动一个新的 shell 进程

	1> self().
	<0.23.0>
	2> x - y.
	** exited: {badarith,[{erl_eval,eval_op,3},
                      {erl_eval,exprs,4},
                      {shell,eval_loop,2}]} **
	3> self().
	<0.40.0>
	

因此,与 <0.23.0> 关联的所有进程都将收到退出信号。除非它们捕获退出,否则它们也会退出。避免这种情况的一种方法是避免将“后台”进程与 shell 进程关联,例如通过使用 spawn 而不是 spawn_link.

9.16  为什么我对浮点运算得到不正确的答案?

一些浮点运算会产生令一些人惊讶的结果。这是一个例子

  5> 1.001 * 1000.
  1000.9999999999999

这不是错误,而是浮点运算的属性,它不是 Erlang 特有的。任何使用浮点运算的语言都会这样。

http://floating-point-gui.de/ 是一个轻松的介绍。 每个计算机科学家应该了解的浮点运算 会深入探讨。

Erlang 的输出格式允许您指定精度,例如

  1> io:fwrite("~.3f\n", [1.001 * 1000]).
  1001.000