模块函数

在 Elixir 中,我们将几个函数组合成模块。我们已经用在前面的章节中,如许多不同模块的String模块

iex> String.length("hello")
5

为了在 Elixir 中创建我们自己的模块,我们使用defmodule宏。我们使用def宏来定义该模块中的函数:

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

iex> Math.sum(1, 2)
3

在下面的章节中,我们的例子会变得更长,并且将它们全部输入到 shell 中可能会非常棘手。现在是我们学习如何编译 Elixir 代码以及如何运行 Elixir 脚本的时候了。

汇编

大多数情况下,将模块写入文件非常方便,因此可以对它们进行编译和重用。假设我们有一个以math.ex下列内容命名的文件:

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

这个文件可以使用elixirc以下编译:

$ elixirc math.ex

这将生成一个名为Elixir.Math.beam包含定义模块字节码的文件。如果我们iex再次启动,我们的模块定义将可用(假设它iex是在字节码文件所在的同一目录中启动的):

iex> Math.sum(1, 2)
3

Elixir 项目通常组织成三个目录:

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

在实际项目中工作时,所调用的构建工具mix将负责为您编译和设置适当的路径。为了学习目的,Elixir还支持更灵活的脚本模式,并且不会生成任何已编译的工件。

脚本模式

除Elixir文件扩展名外.ex,Elixir 还支持.exs用于脚本的文件。Elixir 以完全相同的方式处理这两个文件,唯一的区别在于意图。.ex文件是为了编译而.exs文件用于脚本编写。在执行时,两个扩展都编译并将它们的模块加载到内存中,尽管只有.ex文件以字节格式将它们的字节码写入磁盘.beam

例如,我们可以创建一个名为math.exs

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

IO.puts Math.sum(1, 2)

并执行:

$ elixir math.exs

该文件将被编译到内存中并执行,结果打印“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

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

函数声明还支持守卫和多个子句。如果一个函数有几个子句,Elixir 会尝试每个子句,直到找到一个匹配的子句。下面是一个函数的实现,它检查给定的数字是否为零:

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

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

IO.puts Math.zero?(0)         #=> true
IO.puts Math.zero?(1)         #=> false
IO.puts Math.zero?([1, 2, 3]) #=> ** (FunctionClauseError)
IO.puts Math.zero?(0.0)       #=> ** (FunctionClauseError)

给出与任何条款都不匹配的论点会产生错误。

与类似的结构类似if,命名函数支持两种语法do:do/ end块语法,因为我们已经知道do/ end是关键字列表格式的一种方便的语法。例如,我们可以编辑math.exs看起来像这样:

defmodule Math do
  def zero?(0), do: true
  def zero?(x) when is_integer(x), do: false
end

它会提供相同的行为。您可以使用do:单行,但始终使用do/ end用于跨越多行的功能。

函数捕捉

在本教程中,我们一直使用符号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

记住 Elixir 区分匿名函数和命名函数,前者必须在变量名和括号之间用 dot(.)调用。捕获运算符通过允许将命名函数分配给变量并以与我们分配,调用和传递匿名函数的方式相同的方式传递参数来填补这一空白。

本地或导入的功能,例如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。上面的语法对于短的函数定义很有用。

如果你想从模块中捕获一个功能,你可以这样做&Module.function()

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

&List.flatten(&1, &2)fn(list, tail) -> List.flatten(list, tail) end在这种情况下相当于写作相同&List.flatten/2。你可以阅读更多有关捕获操作&中的Kernel.SpecialForms文档

默认参数

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 \\ "hello") do
    x
  end
end
iex> DefaultTest.dowork
"hello"
iex> DefaultTest.dowork 123
123
iex> DefaultTest.dowork
"hello"

如果具有默认值的函数具有多个子句,则需要创建一个用于声明缺省值的函数头(不带实际主体):

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 将发出以下警告:

warning: this clause cannot match because a previous clause at line 2 always matches

编译器告诉我们,调用join具有两个参数的函数将始终选择第一个定义,join而第二个定义仅在传递三个参数时才会被调用:

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

这完成了我们对模块的简短介绍。在接下来的章节中,我们将学习如何使用命名函数进行递归,探索Elixir词汇指令,这些指令可用于从其他模块导入函数并讨论模块属性。