Elixir入门教程-模块

  1. 编译
  2. 脚本模式
  3. 命名函数
  4. 函数捕获
  5. 默认参数

在Elixir里我们将几个函数到模块里。在前面的章节里我们已经使用了许多不用的模块,比如String模块

1
2
iex> String.length("hello")
5

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

1
2
3
4
5
6
7
8
iex> defmodule Math do
...> def sum(a, b) do
...> a + b
...> end
...> end
iex> Math.sum(1, 2)
3

下面几个部分,我们的例子变得更长,在shell里将可能难以输入。现在是时候我们学习如何编译Elixir代码以及如何运行Elixir脚本。

编译

大多数时候,将模块写入文件是很方便的,因此它们可以被编译和重用。让我们假设我们有一个名字为 math.ex 的文件,它的内容如下:

1
2
3
4
5
defmodule Math do
def sum(a, b) do
a + b
end
end

这个文件可以用 elixirc 来编译:

1
$ elixirc math.ex

这将生成一个名字为 Elixir.Math.beam 的文件,它包含了定义的模块的字节码。如果我们再次启动 iex ,我们的模块定义将有效(被提供,即 iex 和字节码文件相同的目录里启动)。

1
2
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:

1
2
3
4
5
6
7
defmodule Math do
def sum(a, b) do
a + b
end
end
IO.puts Math.sum(1, 2)

并且如下方式执行它:

1
$ elixir math.exs

这个文件将被编译进内存并执行,结果为输出 3 。没有字节码生成。在后续的例子里,我们建议你写你的代码到脚本文件里,并且按上述所示执行它们。

命名函数

在模块里,我们可以用 def/2 定义函数,用 defp/2 定义私有函数。用def/2定义的函数可以被从其他模块调用,而私有函数只能本地调用。

1
2
3
4
5
6
7
8
9
10
11
12
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将尝试每一个分支直到找到匹配的分支为止。如下的例子是检查给定的数字是否是零的函数的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 为如下样子:

1
2
3
4
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 文件:

1
$ iex math.exs
1
2
3
4
5
6
7
8
iex> Math.zero?(0)
true
iex> fun = &Math.zero?/1
&Math.zero?/1
iex> is_function(fun)
true
iex> fun.(0)
true

记住Elixir在匿名函数和命名函数之间做了一个区分,就是前者必需在调用的时候在变量名和圆括号之间加一个点号。捕获运算符通过允许命名函数用我们赋予、调用和传递匿名函数的相同方式被赋予变量并当做参数被传递来消除这种差别。

本地或被导入的函数,比如 is_function/1 ,可以不需要模块的方式被捕获:

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

注意:捕获语法也可以被用来作为创建函数的便捷方式:

1
2
3
4
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() :

1
2
3
4
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里的命名函数也支持默认参数:

1
2
3
4
5
6
7
8
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

任何表达式都允许作为默认值,不过它在函数定义里不会被运算。每次函数调用而且任何它的默认值必须要用的时候,默认值的表达式将被运算:

1
2
3
4
5
defmodule DefaultTest do
def dowork(x \\ "hello") do
x
end
end
1
2
3
4
5
6
iex> DefaultTest.dowork
"hello"
iex> DefaultTest.dowork 123
123
iex> DefaultTest.dowork
"hello"

如果一个有默认值函数有多个分支,它需要创建一个函数头(没有函数体)来声明默认值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

当使用默认值的时候,必须要小心避免重叠函数定义。如下例子:

1
2
3
4
5
6
7
8
9
10
11
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将输出如下警告:

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

编译器告诉我们,用两个参数来调用 join 函数将总是选择 join 函数的第一个定义,而它的第二个定义将只在传入三个参数的时候才被调用:

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

简短的模块知识介绍到此结束。下一章,我们将学习如何用命名函数来递归;探索Elixir的词汇指令,这些指令可以用来从其他模块导入函数;讨论模块属性。

原文链接: http://elixir-lang.org/getting-started/modules.html