8-模块

    Elixir中我们把许多函数组织成一个模块。我们在前几章已经提到了许多模块,
    如:

    创建自己的模块,用宏。用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

    通常把模块写进文件,这样可以编译和重用。假如文件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也提供了脚本模式,可以更灵活而不用编译。

    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定义的函数可以被其它模块中的代码使用,而私有函数仅在定义它的模块内使用。

    函数声明也支持使用卫兵或多个子句。
    如果一个函数有好多子句,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. Math.zero?(0) #=> true
    9. Math.zero?(1) #=> false
    10. Math.zero?([1,2,3])
    11. #=> ** (FunctionClauseError)

    如果没有一个子句能匹配参数,会报错。

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

    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

    任何表达式都可以作为默认参数,但是只在函数调用时 用到了 才被执行。
    (函数定义时,那些表达式只是存在那儿,不执行;函数调用时,没有用到默认值,也不执行)。

    1. iex> DefaultTest.dowork 123
    2. iex> DefaultTest.dowork
    3. hello
    4. :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", "_"

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