在 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 项目通常组织成三个目录:
1._build - 包含编译工件
2.lib - 包含 Elixir 代码(通常是 .ex 文件)
3.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-block 语法,正如我们在上一章中学到的那样。例如,我们可以将 math.exs 编辑为如下所示:
defmodule Math do
def zero?(0), do: true
def zero?(x) when is_integer(x), do: false
end
它将提供相同的行为。您可以将 do: 用于单行代码,但始终将 do-block 用于跨多行的函数。如果您希望保持一致,则可以在整个代码库中使用 do-block。
默认参数
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
Concat.join "Hello", "world"
***First join
"Helloworld"
Concat.join "Hello", "world", "_"
***Second join
"Hello_world"
在这种情况下删除默认参数将修复警告。
至此,我们对模块的简短介绍就结束了。在接下来的章节中,我们将学习如何使用函数定义进行递归,并随后探索与模块相关的更多功能。