查看源代码 模块和函数

在 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 项目通常组织成三个目录

  • _build - 包含编译工件
  • lib - 包含 Elixir 代码(通常为 .ex 文件)
  • test - 包含测试(通常为 .exs 文件)

在处理实际项目时,名为 mix 的构建工具将负责编译并为您设置正确的路径。出于学习和方便的目的,Elixir 还支持一种更灵活且不会生成任何编译工件的脚本模式。

脚本模式

除了 Elixir 文件扩展名 .ex 之外,Elixir 还支持 .exs 文件用于脚本编写。Elixir 对这两种文件以完全相同的方式处理,唯一的区别在于目的。.ex 文件旨在编译,而 .exs 文件用于脚本编写。此约定受到 mix 等项目的遵循。

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

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

IO.puts Math.sum(1, 2)

并执行它为

$ elixir math.exs

因为我们使用了 elixir 而不是 elixirc,所以该模块被编译并加载到内存中,但没有 .beam 文件被写入磁盘。在以下示例中,我们建议您将代码写入脚本文件并按上述方式执行它们。

函数定义

在模块内部,我们可以使用 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)

zero? 中的尾随问号表示此函数返回一个布尔值。要了解有关 Elixir 中模块、函数名称、变量等命名约定的更多信息,请参见 命名约定

给出与任何子句都不匹配的参数将引发错误。

类似于 if 之类的结构,函数定义支持 do:do-块语法,就像我们 在上一章中所学的那样。例如,我们可以编辑 math.exs 使其看起来像这样

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

它将提供相同的行为。您可以对单行使用 do:,但始终对跨越多行的函数使用 do-块。如果您希望保持一致,可以在整个代码库中使用 do-块。

默认参数

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
  # A function head declaring defaults
  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

Elixir 将发出以下警告

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

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

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

在这种情况下,删除默认参数将解决警告。

这结束了我们对模块的简短介绍。在接下来的章节中,我们将学习如何使用函数定义进行递归,并随后探索与模块相关的更多功能。