8-模块

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

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

iex> String.length "hello"
5

创建自己的模块,用defmodule宏。用def宏在其中定义函数:

iex> defmodule Math do
...>   def sum(a, b) do
...>     a + b
...>   end
...> end

iex> Math.sum(1, 2)
3

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

8.1-编译

通常把模块写进文件,这样可以编译和重用。假如文件math.ex有如下内容:

defmodule Math do
  def sum(a, b) do
    a + b
  end
end

这个文件可以用elixirc进行编译:

$ elixirc math.ex

这将生成名为Elixir.Math.beam的bytecode文件。 如果这时再启动iex,那么这个模块就已经可以用了(假如在含有该编译文件的目录启动iex):

iex> Math.sum(1, 2)
3

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

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

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

8.2-脚本模式

除了.ex文件,Elixir还支持.exs脚本文件。 Elixir对两种文件一视同仁,唯一区别是.ex文件会保留编译执行后产出的比特码文件, 而.exs文件用来作脚本执行,不会留下比特码文件。例如,如下创建名为math.exs的文件:

defmodule Math do
  def sum(a, b) do
    a + b
  end
end

IO.puts Math.sum(1, 2)

执行之:

$ elixir math.exs

像这样执行脚本文件时,将在内存中编译和执行,打印出“3”作为结果。没有比特码文件生成。 后文中(为了学习和练习方便),推荐使用脚本模式执行学到的代码。

8.3-命名函数

在某模块中,我们可以用def/2宏定义函数,用defp/2定义私有函数。 用def/2定义的函数可以被其它模块中的代码使用,而私有函数仅在定义它的模块内使用。

defmodule Math do
  def sum(a, b) do
    do_sum(a, b)
  end

  defp do_sum(a, b) do
    a + b
  end
end

Math.sum(1, 2)    #=> 3
Math.do_sum(1, 2) #=> ** (UndefinedFunctionError)

函数声明也支持使用卫兵或多个子句。 如果一个函数有好多子句,Elixir会匹配每一个子句直到找到一个匹配的。 下面例子检查参数是否是数字:

defmodule Math do
  def zero?(0) do
    true
  end

  def zero?(x) when is_number(x) do
    false
  end
end

Math.zero?(0)  #=> true
Math.zero?(1)  #=> false

Math.zero?([1,2,3])
#=> ** (FunctionClauseError)

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

8.4-函数捕捉

本教程中提到函数,都是用name/arity的形式描述。 这种表示方法可以被用来获取一个命名函数(赋给一个函数型变量)。 下面用iex执行一下上文定义的math.exs文件:

$ iex math.exs
iex> Math.zero?(0)
true
iex> fun = &Math.zero?/1
&Math.zero?/1
iex> is_function fun
true
iex> fun.(0)
true

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

本地定义的,或者已导入的函数,比如is_function/1,可以不用前缀模模块名:

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

这种语法还可以作为快捷方式来创建和使用函数:

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

代码中&1 表示传给该函数的第一个参数。 因此,&(&1+1)其实等同于fn x->x+1 end。在创建短小函数时,这个很方便。 想要了解更多关于&捕捉操作符,参考Kernel.SpecialForms文档

8.5-默认参数

Elixir中,命名函数也支持默认参数:

defmodule Concat do
  def join(a, b, sep \\ " ") do
    a <> sep <> b
  end
end

IO.puts Concat.join("Hello", "world")      #=> Hello world
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world

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

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

如果有默认参数值的函数有了多条子句,推荐先定义一个函数头(无具体函数体)声明默认参数:

defmodule Concat do
  def join(a, b \\ nil, sep \\ " ")

  def join(a, b, _sep) when is_nil(b) do
    a
  end

  def join(a, b, sep) do
    a <> sep <> b
  end
end

IO.puts Concat.join("Hello", "world")      #=> Hello world
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world
IO.puts Concat.join("Hello")               #=> Hello

使用默认值时,注意对函数重载会有一定影响。考虑下面例子:

defmodule Concat do
  def join(a, b) do
    IO.puts "***First join"
    a <> b
  end

  def join(a, b, sep \\ " ") do
    IO.puts "***Second join"
    a <> sep <> b
  end
end

如果将以上代码保存在文件“concat.ex”中并编译,Elixir会报出以下警告:

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

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

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

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