8-模塊
======
[編譯](#81-%E7%BC%96%E8%AF%91)
[腳本模式](#82-%E8%84%9A%E6%9C%AC%E6%A8%A1%E5%BC%8F)
[命名函數](#83-%E5%91%BD%E5%90%8D%E5%87%BD%E6%95%B0)
[函數捕捉](#84-%E5%87%BD%E6%95%B0%E6%8D%95%E6%8D%89)
[默認參數](#85-%E9%BB%98%E8%AE%A4%E5%8F%82%E6%95%B0)
Elixir中我們把許多函數組織成一個模塊。我們在前幾章已經提到了許多模塊,
如[String模塊](http://elixir-lang.org/docs/stable/elixir/String.html):
```elixir
iex> String.length "hello"
5
```
創建自己的模塊,用```defmodule```宏。用```def```宏在其中定義函數:
```elixir
iex> defmodule Math do
...> def sum(a, b) do
...> a + b
...> end
...> end
iex> Math.sum(1, 2)
3
```
>像ruby一樣,模塊名大寫起頭
## 8.1-編譯
通常把模塊寫進文件,這樣可以編譯和重用。假如文件```math.ex```有如下內容:
```elixir
defmodule Math do
def sum(a, b) do
a + b
end
end
```
這個文件可以用```elixirc```進行編譯:
```elixir
$ elixirc math.ex
```
這將生成名為```Elixir.Math.beam```的bytecode文件。
如果這時再啟動iex,那么這個模塊就已經可以用了(假如在含有該編譯文件的目錄啟動iex):
```elixir
iex> Math.sum(1, 2)
3
```
Elixir工程通常組織在三個文件夾里:
- ebin,包括編譯后的字節碼
- lib,包括Elixir代碼(.ex文件)
- test,測試代碼(.exs文件)
實際項目中,構建工具Mix會負責編譯,并且設置好正確的路徑。
而為了學習方便,Elixir也提供了腳本模式,可以更靈活而不用編譯。
## 8.2-腳本模式
除了.ex文件,Elixir還支持.exs腳本文件。
Elixir對兩種文件一視同仁,唯一區別是.ex文件會保留編譯執行后產出的比特碼文件,
而.exs文件用來作腳本執行,不會留下比特碼文件。例如,如下創建名為math.exs的文件:
```elixir
defmodule Math do
def sum(a, b) do
a + b
end
end
IO.puts Math.sum(1, 2)
```
執行之:
```sh
$ elixir math.exs
```
像這樣執行腳本文件時,將在內存中編譯和執行,打印出“3”作為結果。沒有比特碼文件生成。
后文中(為了學習和練習方便),推薦使用腳本模式執行學到的代碼。
## 8.3-命名函數
在某模塊中,我們可以用```def/2```宏定義函數,用```defp/2```定義私有函數。
用```def/2```定義的函數可以被其它模塊中的代碼使用,而私有函數僅在定義它的模塊內使用。
```elixir
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)
```
函數聲明也支持使用衛兵或多個子句。
如果一個函數有好多子句,Elixir會匹配每一個子句直到找到一個匹配的。
下面例子檢查參數是否是數字:
```elixir
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-函數捕捉
本教程中提到函數,都是用```name/arity```的形式描述。
這種表示方法可以被用來獲取一個命名函數(賦給一個函數型變量)。
下面用iex執行一下上文定義的math.exs文件:
```elixir
$ iex math.exs
```
```elixir
iex> Math.zero?(0)
true
iex> fun = &Math.zero?/1
&Math.zero?/1
iex> is_function fun
true
iex> fun.(0)
true
```
用```&<function notation>```通過函數名捕捉一個函數,它本身代表該函數值(函數類型的值)。
它可以不必賦給一個變量,直接用括號來使用該函數。
本地定義的,或者已導入的函數,比如```is_function/1```,可以不用前綴模模塊名:
```elixir
iex> &is_function/1
&:erlang.is_function/1
iex> (&is_function/1).(fun)
true
```
這種語法還可以作為快捷方式來創建和使用函數:
```elixir
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```。在創建短小函數時,這個很方便。
想要了解更多關于```&```捕捉操作符,參考[Kernel.SpecialForms文檔](http://elixir-lang.org/docs/stable/elixir/Kernel.SpecialForms.html)。
## 8.5-默認參數
Elixir中,命名函數也支持默認參數:
```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
```
任何表達式都可以作為默認參數,但是只在函數調用時 **用到了** 才被執行。
(函數定義時,那些表達式只是存在那兒,不執行;函數調用時,沒有用到默認值,也不執行)。
```elixir
defmodule DefaultTest do
def dowork(x \\ IO.puts "hello") do
x
end
end
```
```elixir
iex> DefaultTest.dowork 123
123
iex> DefaultTest.dowork
hello
:ok
```
如果有默認參數值的函數有了多條子句,推薦先定義一個函數頭(無具體函數體)聲明默認參數:
```elixir
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
```
使用默認值時,注意對函數重載會有一定影響。考慮下面例子:
```elixir
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會報出以下警告:
```elixir
concat.ex:7: this clause cannot match because a previous clause at line 2 always matches
```
編譯器是在警告我們,在使用兩個參數調用```join```函數時,總使用第一個函數定義。
只有使用三個參數調用時,才會使用第二個定義:
```elixir
$ iex concat.exs
```
```elixir
iex> Concat.join "Hello", "world"
***First join
"Helloworld"
iex> Concat.join "Hello", "world", "_"
***Second join
"Hello_world"
```
后面幾章將介紹使用命名函數來做循環,如何從別的模塊中導入函數,以及模塊的屬性等。