模块

在Elixir中,相关的函数会被组合到一起,称为模块。在之前的章节中,我们已经用到了很多模块,例如字符串模块

  1. iex> String.length "hello"
  2. 5

要在Elixir中创建我们自己的模块,我需要使用宏defmodule。我们使用另一个宏def在模块中定义函数:

  1. iex> defmodule Math do
  2. ...> def sum(a, b) do
  3. ...> a + b
  4. ...> end
  5. ...> end
  6. iex> Math.sum(1, 2)
  7. 3

在接下去的部分中,我们的例子将会变得更加复杂,并且可能不太容易手动输入进iex里。不过乘此机会,我们正好可以学习一下如何编译Elixir代码和如执行Elixir脚本。

8.1 编译

在大部分的时候最好将模块写入文件,以便于编译和重用。这里我们假定我们已经有了一个文件math.ex,包含如下的代码:

  1. defmodule Math do
  2. def sum(a, b) do
  3. a + b
  4. end
  5. end

我们可以用elixirc来编译这个文件:

  1. elixirc math.ex

这会产生一个对应模块的字节码文件的Elixir.Math.beam。如果在这个文件所在的目录,我们重新开始iex, 我们先前定义的模块就可以使用了。

  1. iex> Math.sum(1, 2)
  2. 3

Elixir的项目通常会包含至少以下三个目录:

  • ebin - 包含编译后的字节码
  • lib - 包含Elixir源代码 (通常是.ex文件)
  • test - 包含测试(通常是 .exs文件)

在实际的项目中,用到的编译工具是mix,它负责设置正确的路径并编译。为了方便学习,Elixir也提供了一个更加灵活的脚本模式,无需编译可以之间运行。

8.2 脚本模式

除了常见的Elixir源码文件扩展名.ex,Elixir还支持.exs文件作为脚本。Elixir对两种文件是一视同仁的,唯一的区别在于.ex文件必须编译的,而.exs无需编译就可以之间运行。举例来说,我们可以创建一个叫math.exs的文件:

  1. defmodule Math do
  2. def sum(a, b) do
  3. a + b
  4. end
  5. end
  6. IO.puts Math.sum(1, 2)

然后执行文件:

  1. elixir math.exs

这个文件将会被在内存中编译并执行,然后打印出结果“3”。它不产生字节码。对于下面的例子,我们建议你把你的代码写入脚本中,然后按照上面的方式执行。

8.3 有名函数

在模块内部,我们可以用def/2定义函数,用defp/2定义私有函数。用def/2定义的函数能够外部的模块调用而私有函数只能被从模块内部使用。

  1. defmodule Math do
  2. def sum(a, b) do
  3. do_sum(a, b)
  4. end
  5. defp do_sum(a, b) do
  6. a + b
  7. end
  8. end
  9. Math.sum(1, 2) #=> 3
  10. Math.do_sum(1, 2) #=> ** (UndefinedFunctionError)

函数申明同时也支持守护和多子句。如果一个函数有多个子句,Elixir会尝试每一个子句直到发现匹配的那个。下面的例子实现了一个函数去检查输入的数字是否是零:

  1. defmodule Math do
  2. def zero?(0) do
  3. true
  4. end
  5. def zero?(x) when is_number(x) do
  6. false
  7. end
  8. end
  9. Math.zero?(0) #=> true
  10. Math.zero?(1) #=> false
  11. Math.zero?([1,2,3])
  12. #=> ** (FunctionClauseError)

如果一个参数无法匹配任何一个子句,会导致一个错误。

8.4 函数捕捉

在这篇教程中,我们一直都用函数名/参数量的方式指向函数。用这种方式也可以用来获取模块中的有名函数。让我们重新打开iex,并运行之前定义的math.exs脚本:

  1. $ iex math.exs
  1. iex> Math.zero?(0)
  2. true
  3. iex> fun = &Math.zero?/1
  4. &Math.zero?/1
  5. iex> is_function fun
  6. true
  7. iex> fun.(0)
  8. true

本地函数或者已经引入的其他模块的函数,比如is_function/1, 没有模块也可以被捕捉。

  1. iex> &is_function/1
  2. &:erlang.is_function/1
  3. iex> (&is_function/1).(fun)
  4. true

注意捕捉语法也是一种定义函数的快捷方式:

  1. iex> fun = &(&1 + 1)
  2. #Function<6.71889879/1 in :erl_eval.expr/5>
  3. iex> fun.(1)
  4. 2

上面例子中&1是传给函数的第一个参数。&(&1 + 1)等价于fn x -> x + 1 end。上面的语法适合于定义短小的函数。更多的关于函数捕捉操作符&,请参考Kernel.SpecialForms文档

8.5 默认参数

Elixir中的有名函数也支持默认参数:

  1. defmodule Concat do
  2. def join(a, b, sep \\ " ") do
  3. a <> sep <> b
  4. end
  5. end
  6. IO.puts Concat.join("Hello", "world") #=> Hello world
  7. IO.puts Concat.join("Hello", "world", "_") #=> Hello_world

默认参数值可以是任何一个表达式,但不会在函数定义时执行。它只是被存储在哪里。每次函数被调用的时候,所有的默认参数值都会被使用,代表参数值的表达式就会被执行:

  1. defmodule DefaultTest do
  2. def dowork(x \\ IO.puts "hello") do
  3. x
  4. end
  5. end
  1. iex> DefaultTest.dowork 123
  2. 123
  3. iex> DefaultTest.dowork
  4. hello
  5. :ok

在一个一个带有默认参数的函数有多个子句,我们建议创建一个函数头(无需实际的函数体),专用于申明默认参数:

  1. defmodule Concat do
  2. def join(a, b \\ nil, sep \\ " ")
  3. def join(a, b, _sep) when nil?(b) do
  4. a
  5. end
  6. def join(a, b, sep) do
  7. a <> sep <> b
  8. end
  9. end
  10. IO.puts Concat.join("Hello", "world") #=> Hello world
  11. IO.puts Concat.join("Hello", "world", "_") #=> Hello_world
  12. IO.puts Concat.join("Hello") #=> Hello

在使用默认参数值的时候,注意避免覆盖函数定义。考虑下面的例子:

  1. defmodule Concat do
  2. def join(a, b) do
  3. IO.puts "***First join"
  4. a <> b
  5. end
  6. def join(a, b, sep \\ " ") do
  7. IO.puts "***Second join"
  8. a <> sep <> b
  9. end
  10. end

如果我们把上面的代码保存到一个文件“concat.ex”,并编译。Elixir会发出下面的警告:

  1. concat.exs:7: this clause cannot match because a previous clause at line 2 always matches

编译器在告诉我们当用两个参数代用函数join只会选择join的第一个定义,而第二个定义只有在三个参数的时候才会被选中。

  1. $ iex concat.exs
  1. iex> Concat.join "Hello", "world"
  2. ***First join
  3. "Helloworld"
  1. iex> Concat.join "Hello", "world", "_"
  2. ***Second join
  3. "Hello_world"

到这里我们对模块的简介就结束了。在下面的几章,我们将学习如何用函数递归,Elixir中的可以从别的模块中引入函数的语法工具,以及讨论模块的属性。