调试
臭虫(Bugs)可谓是任何项目都无法避免的存在,所以调试也不可或缺。
本课程我们将学习如何调试 Elixir 代码,并使用静态分析工具来帮助寻找可能存在的 bugs
IEx
调试 Elixir 代码最直接的工具就是 IEx 了。
但是,别被它的简单而愚弄到你了 - 你可以通过它来解决你应用的大部分问题。
IEx 全称是 Elixir 的交互式 shell
。
你可能已经在前面的基础课那里了解到它了。
调试的做法其实很简单。我们就在需要调试的地方,通过交互式的 shell 来操作。
首先,创建一个 test.exs
来试验一下吧。在文件里输入以下代码:
defmodule TestMod do
def sum([a, b]) do
b = 0
a + b
end
end
IO.puts(TestMod.sum([34, 65]))
如果你执行这个文件,输出显然为 34
:
$ elixir test.exs
warning: variable "b" is unused (if the variable is not meant to be used, prefix it with an underscore)
test.exs:2
34
现在我们来看看如何调试这段代码,也就是最令人兴奋的地方。
紧接着在 b = 0
那行的后面加入 require IEx; IEx.pry
这一行,然后重新运行一次代码。
你将会看到类似这样的输出:
$ elixir test.exs
warning: variable "b" is unused (if the variable is not meant to be used, prefix it with an underscore)
test.exs:2
Cannot pry #PID<0.92.0> at TestMod.sum/1 (test.exs:5). Is an IEx shell running?
34
注意到那条重要的提醒消息了吗?
向以往那样运行一个程序的时候,IEx 会输出这条消息,而不会阻止程序的运行。
如果我们希望正确的进入调试模式,则需要在命令的前面使用 iex -S
。
这么做的意义就是在 iex
命令里面运行 mix
,从而应用程序可以在一种特殊模式下运行,比如对 IEx.pry
的调用可以把应用程序暂停下来。
比如说,iex -S mix phx.server
这么就可以调试你的 Phoenix 应用。
对我们上面的例子来说,则是通过 iex -r test.exs
来执行文件:
$ iex -r test.exs
Erlang/OTP 21 [erts-10.3.1] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]
warning: variable "b" is unused (if the variable is not meant to be used, prefix it with an underscore)
test.exs:2
Request to pry #PID<0.107.0> at TestMod.sum/1 (test.exs:5)
3: b = 0
4:
5: require IEx; IEx.pry
6:
7: a + b
Allow? [Yn]
当你输入 y
或者回车来给予反馈后,你将进入交互执行模式。
$ iex -r test.exs
Erlang/OTP 21 [erts-10.3.1] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]
warning: variable "b" is unused (if the variable is not meant to be used, prefix it with an underscore)
test.exs:2
Request to pry #PID<0.107.0> at TestMod.sum/1 (test.exs:5)
3: b = 0
4:
5: require IEx; IEx.pry
6:
7: a + b
Allow? [Yn] y
Interactive Elixir (1.8.1) - press Ctrl+C to exit (type h() ENTER for help)
pry(1)> a
34
pry(2)> b
0
pry(3)> a + b
34
pry(4)> continue
34
Interactive Elixir (1.8.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
(v)ersion (k)ill (D)b-tables (d)istribution
要退出 IEx,你可以通过按 Ctrl+C
两次,或者输入 continue
来继续运行到下一个断点。
正如你所见,你可以运行任何 Elixir 代码。
这么做的局限在于,语言的不可变特性,使得你无法更改当前代码的变量值。
但是,你可以获取任何变量值,并进行任何计算。
上面的案例,bug 出在 b
被重新赋予了 0,sum
函数也因此出现问题。
当然,语言层面在最初的时候已经发现了这个 bug,但这只是一个例子!
IEx.Helpers
其中一个最让人讨厌的 IEx 使用经验就是它不支持查询以前执行过的命令。
为了解决这个问题,IEx 文档里面有专门的一个章节告诉你如何解决这个问题。
你还可以通过 IEx.Helpers 文档来查看可用的一些辅助工具有哪些。
Dialyxir 和 Dialyzer
Dialyzer,全称就是 DIscrepancy AnaLYZer for ERlang,它是一个静态代码分析工具。也就是说,它_阅读_和分析你的代码,但是不_执行_它们,比如说,寻找 bugs,无法触及的死代码等。
Dialyxir 则是在 Elixir 里简化了 Dialyzer 使用的 mix 任务。
Specification 可以帮助像 Dialyzer 这样的工具更好地理解代码。不像那些只能被人类阅读和理解的文档(假如存在或者写的足够好的话),@spec
使用了一些可以被机器理解的,更规范的语法。
让我们把 Dialyxir 加到我们的项目里头吧。最简单的方式就是添加依赖到 mix.exs
文件中:
defp deps do
[{:dialyxir, "~> 0.4", only: [:dev]}]
end
然后调用一下面的命令:
$ mix deps.get
...
$ mix deps.compile
第一个命令下载并安装 Dialyxir。或许你会同时被要求安装 Hex。第二个命令编译 Dialyxir 应用。如果你想全局安装 Dialyxir,请参考它的文档。
最后一个步骤就是要运行 Dialyzer 来重建 PLT(Persistent Lookup Table)。每次安装新版本的 Erlang 或者 Elixir 后,你都要做这一步。幸运地是,Dialyzer 不会每次使用的时候都去分析标准程序库。这个重建过程需要好几分钟才能下载完成。
$ mix dialyzer --plt
Starting PLT Core Build ... this will take awhile
dialyzer --build_plt --output_plt /.dialyxir_core_18_1.3.2.plt --apps erts kernel stdlib crypto public_key -r /Elixir/lib/elixir/../eex/ebin /Elixir/lib/elixir/../elixir/ebin /Elixir/lib/elixir/../ex_unit/ebin /Elixir/lib/elixir/../iex/ebin /Elixir/lib/elixir/../logger/ebin /Elixir/lib/elixir/../mix/ebin
Creating PLT /.dialyxir_core_18_1.3.2.plt ...
...
done in 5m14.67s
done (warnings were emitted)
代码静态分析
Dialyxir 已经准备好了:
$ mix dialyzer
...
examples.ex:3: Invalid type specification for function 'Elixir.Examples':sum_times/1. The success typing is (_) -> number()
...
Dialyzer 提示的信息已经很明确了:函数 sum_times/1
的返回值,和声明的不一样。因为 Enum.sum/1
返回的是 number
而不是一个 integer
。但是 sum_times/1
的返回值声明为 integer
。
因为 number
并不是 integer
,所以我们就得到了一个错误。那该怎么修改?我们可以使用 round/1
函数把 number
类型转换为 integer
:
@spec sum_times(integer) :: integer
def sum_times(a) do
[1, 2, 3]
|> Enum.map(fn el -> el * a end)
|> Enum.sum()
|> round
end
最后:
$ mix dialyzer
...
Proceeding with analysis... done in 0m0.95s
done (passed successfully)
借助 specifications 就可以使用工具进行静态代码分析,并让代码变得更健壮和包含更少的 bugs。
调试
有时,静态代码分析是不够的。要找到 bugs,理解代码的执行过程是必须的。最简单的方案发就是在代码里加上诸如 IO.puts/2
这样的代码来打印输出一些语句以便跟踪值的变化和代码执行过程。但是,这样的手段非常的原始,并有很多局限性。庆幸的是,我们可以使用 Erlang 的调试器来调试 Elixir 代码。
现在我们来看一个基本的模块:
defmodule Example do
def cpu_burns(a, b, c) do
x = a * 2
y = b * 3
z = c * 5
x + y + z
end
end
然后运行 iex
:
iex -S mix
再运行调试器:
iex > :debugger.start()
{:ok, #PID<0.307.0>}
Erlang 的 :debugger
模块可以让我们访问调试器。我们可以使用 start/1
函数来对它进行配置:
- 通过传入文件路径来指定外部配置文件。
-
如果参数是
:local
或者:global
,调试器就会:-
:global
- 调试器会解析所有已知节点的代码。这个是选项的默认值。 -
:local
- 调试器只会解析当前节点的代码。
-
下一步就是把调试器挂载到我们的模块上:
iex > :int.ni(Example)
{:module, Example}
:int
模块是一个解析器。它能让我们创建断点,和一步步执行代码。
当你打开调试器的时候,会出现类似的新窗口:
当我们把调试器挂载到要调试的模块时,它就会出现在左边的菜单:
创建断点
一个断点就是代码执行到指定位置后,会挂起的地方。有两种创建断点的方法:
-
在代码里调用
:int.break/2
- 通过调试器界面
让我们先在 IEx 尝试添加一个断点:
iex > :int.break(Example, 8)
:ok
这在 Example
模块代码的第 8 行处设定了一个断点。然后开始调用我们的函数:
iex > Example.cpu_burns(1, 1, 1)
在 IEx 中,代码的运行被挂起了,调试器窗口显示如下:
另一个窗口会显示出当前执行的源代码:
在这个窗口中,我们可以查看变量的值,跳到下一行代码,或者执行表达式。调用 :int.disable_break/2
就能够禁用断点:
iex > :int.disable_break(Example, 8)
:ok
我们可以调用 :int.enable_break/2
来重新开启一个断点,或者通过如下命令删除断点:
iex > :int.delete_break(Example, 8)
:ok
在调试器窗口也有同样的操作选项。在顶层菜单,Break,我们可以选择 Line Break 并设置断点。如果我们选择了一行没有代码的涤烦设置了断点,它会被忽略,但是依然会在调试器窗口出现。断点的类型有三种:
-
行断点 - 调试器到达目标行时,挂起执行。这种断点通过
:int.break/2
设置。 -
条件断点 - 和行断点类似,但是调试器只有在满足特定条件的时候才会挂起代码。
:int.get_binding/2
可以获取绑定的条件变量。 -
函数断点 - 调试器会在函数的第一行挂起。这种断点通过
:int.break_in/3
配置。
准备就绪!调试快乐!
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!