8-模組

StraightDave發表於2014-09-12

8-模組

編譯
指令碼模式
命名函式
函式捕捉
預設引數

Elixir中我們把許多函式組織成一個模組。我們在前幾章已經提到了許多模組,如String模組

iex> String.length "hello"
5

建立自己的模組,用defmodule巨集。用def巨集在其中定義函式:

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

iex> Math.sum(1, 2)
3

像ruby一樣,模組名大寫起頭

8.1-編譯

通常把模組寫進檔案,這樣可以編譯和重用。假如檔案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工程通常組織在三個資料夾裡:

  • ebin,放置編譯後的位元組碼(.beam檔案)
  • lib,放置Elixir原始碼(.ex檔案)
  • test,測試程式碼(.exs指令碼檔案)

實際專案中,構建工具Mix會負責編譯,並且設定好正確的路徑。 而為了學習方便,Elixir也提供了指令碼模式,可以更靈活而不用編譯。

8.2-指令碼模式

除了.ex檔案,Elixir還支援.exs指令碼檔案。Elixir對兩種檔案一視同仁,唯一區別是.ex檔案有待編譯, 而.exs檔案用來作指令碼執行,不需要編譯。例如,如下建立名為math.exs的檔案:

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

IO.puts Math.sum(1, 2)

執行之:

$ elixir math.exs

檔案將在記憶體中編譯和執行,列印出“3”作為結果。沒有位元組碼檔案生成。
後文中(為了學習和練習方便),推薦使用指令碼模式執行學到的程式碼。

8.3-命名函式

在某模組中,我們可以用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

Math.sum(1, 2)    #=> 3
Math.do_sum(1, 2) #=> ** (UndefinedFunctionError)

函式宣告也支援使用衛兵或多個子句。 如果一個函式有好多子句,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檔案:

$ 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

&<function notation>從函式名捕捉一個函式,它本身代表該函式值(函式型別的值)。 它可以不必賦給一個變數,直接用括號來使用該函式。 當前上下文定義的,或者已匯入的函式,比如is_function/1,可以不字首模組名:

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

這種語法還可以作為快捷方式使用來建立函式:

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文件

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

如果有預設引數值的函式有了多條子句,推薦先定義一個函式頭(無具體函式體)僅為了宣告這些預設值:

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

使用預設值時,注意對函式過載會有一定影響。考慮下面例子:

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會報出以下警告:

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

編譯器是在警告我們,在使用兩個引數呼叫join函式時,總使用第一個函式定義。 只有使用三個引數呼叫時,才會使用第二個定義:

$ iex concat.exs

.

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

後面幾章將介紹使用命名函式來做迴圈,如何從別的模組中匯入函式,以及模組的屬性等。

相關文章