1

elixir-lang.org 8 Modules

8 Module

在Elixir中,module是一系列函数的集合我们已经在先前的章节中使用过许多不同的module,比如 String module

iex> String.length "hello"
5

使用defmodule宏在Elixir定义自己的module,使用def宏定义函数,比如:

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

iex> Math.sum(1, 2)
3

在下面的章节中,代码示例可以有点复杂,可能不太好在shell中测试这些代码,不过我们可以学习如何编译运行代码或者怎么以script的形式运行。

8.1 编译

通常我们会把一个module在一个文件中编写在以方便编译,重用,比如我们有一个math.ex 的Elixir代码文件,内容是这样的:

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

使用elixirc这个工具进行编译:

elixirc math.ex

这个命令会生成 Elixir.Math.beam 文件,此文件是编译生成的字节码,我们的math module 应该就可以用了,启动iex,注意要和beam文件在同一个目录

iex> Math.sum(1, 2)
3

Elixir 项目通常将代码组织到三个目录 :

  • ebin - 编译成的字节码
  • lib - elixir代码,后缀.ex
  • test - 测试文件 ,后缀.exs

在实际工程中,Elixir的构建工具mix会帮你编译代码,设置paths。为了方便学习,Elixir也支持以脚本的形式运行你的代码。脚本形式不会生成编译文件。

8.2 脚本模式

除了Elixir的.ex 文件扩展,Elixir 也支持 .exs 扩展用于脚本模式。Elixir处理两中文件的方式基本相同,唯一不同的是脚本不需要编译。
.exs 后缀的文件是Elixir 脚本文件,意思是以脚本的方式运行Elixir代码, 例如:创建math.exs文件

math.exs:

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

IO.puts to_string(Math.sum(1, 2))

这样执行

elixir math.exs

8.3 函数

在模块内,我们可以使用 def/2 定义函数,使用 defp/2 定义私有函数。使用def/2 定义的函数可以在其他module调用,私有函数只能module内调用, 比如:

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)

函数声明中同样支持卫语句与多子句,如果一个函数有多个子句(multiple clauses),Elixir会尝试每一个子句,直到有一个匹配。下边实现了个函数,检测参数是0还是其它的

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 函数捕获(Function capturing)

在本教程中,我们已经使用的符号 名字/参数数量(name/arity) 来引用函数。可以使用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

本地或者已经导入的函数,比如 is_function/1,不用加module 名字就可以捕获:

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

注意capture语法也可以用作创建函数的快捷方式:

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

&1 表示传递到函数的第一个参数, &(&1+1) 展开就是 fn x -> x + 1,你可以在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

如果一个函数默认值有多个不同的子句(multiple clauses), 建议定义一个单独的头函数,此函数没有函数体,仅用作默认值的声明:

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

  def join(a, b, _sep) when 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

定义函数使用默认值时,注意要避免交叉的函数定义,比如下面的例子,第二个join是sep是空的时候,会匹配到两个函数。

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:

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

编译器告诉我们当调用join这个函数时,如果只有两个参数,只会用到第一个join的定义,第二个只会在第三个参数存在的时候调用。

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

lidashuang
6.7k 声望165 粉丝

$ Ruby/Elixir/Golang