模块

  1. 1. 编译
  2. 2. 脚本模式
  3. 3. 具名函数
  4. 4. 函数捕获
  5. 5. 默认参数

在Elixir中我们将一些函数集合到模块里。在之前的章节里我们已经使用了许多不同的模块,例如String模块:

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

为了创造我们自己的模块,需要用到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

在接下来的部分,我们的例子会变得更长,若把它们全部输入终端则会变得很复杂。是时候学习如何编译Elixir代码以及如何运行Elixir脚本了。

编译

大多数时候我们都会将模块写入文件,方便编译和复用。假设我们有一个名为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,将可以使用我们的模块定义(需要在字节码文件存在的目录中启动iex):

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

Elixir工程通常由三个目录组成:

  1. - ebin —— 包含了编译好的字节码
  2. - lib —— 包含了elixir代码(通常是`.ex`文件)
  3. - test —— 包含了测试文件(通常是`.exs`文件)

在实践中,构建工具mix将会为你编译和设置好路径。出于学习目的,Elixir也支持脚本模式,它更加灵活并且不会生成任何编译后的火星文字。

脚本模式

除了后缀名为.ex的Elixir文件,Elixir也支持用于执行脚本的.exs文件。Ellixir对待它们几乎完全一样,唯一的不同是目的。.ex文件需要被编译,.exs文件用于执行脚本。在执行时,它们都会被编译并将它们的模块载入到内存里,尽管只有.ex文件会将它的字节码以.beam格式写入硬盘。

举个例子,我们可以创造一个名为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”作为结果。不会创建字节码文件。在接下来的例子中,我们建议你将代码写入脚本文件并以上述方法执行。

具名函数

在模块中,我们可以使用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. IO.puts Math.sum(1, 2) #=> 3
  10. IO.puts Math.do_sum(1, 2) #=> ** (UndefinedFunctionError)

函数声明也支持卫语句和多重从句。如果一个函数有多个从句,Elicir会逐个尝试知道有一个匹配。下面定义了一个检查数字是否为零的函数:

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

对于不匹配任何从句的参数会抛出一个异常。

if结构相似,具名函数也支持do:do/end块语法,我们已经知道do/end语法只不过是关键字列表的简写形式。例如,我们可以这样修改math.exs文件:

  1. defmodule Math do
  2. def zero?(0), do: true
  3. def zero?(x) when is_integer(x), do: false
  4. end

它们效果是一样的。你可以用do:来写一行的代码,但对于多行代码还是要用do/end

函数捕获

在前文中,我们一直使用name/arity的记号来指代函数。我们的确可以使用这种记号法来获取某个具名函数。打开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。这种语法很适用于短的函数定义。

如果你想捕获一个模块中的函数,你可以使用&Module.function()

  1. iex> fun = &List.flatten(&1, &2)
  2. &List.flatten/2
  3. iex> fun.([1, [[2], 3]], [4, 5])
  4. [1, 2, 3, 4, 5]

&List.flatten(&1, &2)等同于fn(list, tail) -> List.flatten(list, tail) end,在这种情况下和&List.flatten/2是一样的。你可以在Kernel.SpecialForms文档中找到更多有关捕获符号&的信息。

默认参数

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
  2. hello
  3. :ok
  4. iex> DefaultTest.dowork 123
  5. 123
  6. iex> DefaultTest.dowork
  7. hello
  8. :ok

当带默认值的函数有多个从句时,就需要创建一个不包含函数内容的函数头来声明默认值:

  1. defmodule Concat do
  2. def join(a, b \\ nil, sep \\ " ")
  3. def join(a, b, _sep) when is_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.ex:7: warning: 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中可以从其他模块里导入函数的词汇命令,以及讨论模块的属性。