8-模块

编译
脚本模式
命名函数
函数捕捉
默认参数

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

像ruby一样,模块名大写起头

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的bytecode文件。
如果这时再启动iex,那么这个模块就已经可以用了(假如在含有该编译文件的目录启动iex):

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

Elixir工程通常组织在三个文件夹里:

  • ebin,包括编译后的字节码
  • lib,包括Elixir代码(.ex文件)
  • test,测试代码(.exs文件)

实际项目中,构建工具Mix会负责编译,并且设置好正确的路径。
而为了学习方便,Elixir也提供了脚本模式,可以更灵活而不用编译。

8.2-脚本模式

除了.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-函数捕捉

本教程中提到函数,都是用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

&<function notation>通过函数名捕捉一个函数,它本身代表该函数值(函数类型的值)。
它可以不必赋给一个变量,直接用括号来使用该函数。

本地定义的,或者已导入的函数,比如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 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: this clause cannot match because a previous clause at line 2 always matches

编译器是在警告我们,在使用两个参数调用join函数时,总使用第一个函数定义。
只有使用三个参数调用时,才会使用第二个定义:

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

后面几章将介绍使用命名函数来做循环,如何从别的模块中导入函数,以及模块的属性等。