Do you want to pick up from where you left of?
Take me there

模組

我們從經驗中知道,把所有的函數放在同一個文件和範圍 (scope) 內是不合理的。 在本課程中,我們將介紹如何對函數進行分組,並定義一個稱為結構體 (struct) 的特殊映射,以便更有效地組織我們的程式碼。

Table of Contents

模組

模組允許我們將函數整合到一個命名空間 (namespace) 中。 除了對函數進行分組之外,它同時允許我們定義在 函數 中介紹的命名函數和私有函數。

現在來看看一個基本的例子:

defmodule Example do
  def greeting(name) do
    "Hello #{name}."
  end
end

iex> Example.greeting "Sean"
"Hello Sean."

在 Elixir 中能夠使用巢狀模組,使您可以進一步定義多層命名空間:

defmodule Example.Greetings do
  def morning(name) do
    "Good morning #{name}."
  end

  def evening(name) do
    "Good night #{name}."
  end
end

iex> Example.Greetings.morning "Sean"
"Good morning Sean."

模組屬性 (Module Attributes)

模組屬性在 Elixir 中最常以常數做為使用。 現在來看一個簡單的例子:

defmodule Example do
  @greeting "Hello"

  def greeting(name) do
    ~s(#{@greeting} #{name}.)
  end
end

注意 Elixir 有些保留屬性 (reserved attributes)。 最常見的三個是:

結構體 (Structs)

結構體是具有一組被定義的鍵 (keys) 和預設值的特殊映射。 結構體必須定義在一個模組中,因此必須通過模組來存取。 在模組中,單只定義結構體是常見用法。

為了定義一個結構體,我們使用 defstruct 和關鍵字列表以及預設值:

defmodule Example.User do
  defstruct name: "Sean", roles: []
end

現在來創設一些結構:

iex> %Example.User{}
%Example.User<name: "Sean", roles: [], ...>

iex> %Example.User{name: "Steve"}
%Example.User<name: "Steve", roles: [], ...>

iex> %Example.User{name: "Steve", roles: [:manager]}
%Example.User<name: "Steve", roles: [:manager]>

我們可以像更新映射一樣更新結構體:

iex> steve = %Example.User{name: "Steve"}
%Example.User<name: "Steve", roles: [...], ...>
iex> sean = %{steve | name: "Sean"}
%Example.User<name: "Sean", roles: [...], ...>

最重要的是,你可以將結構體與映射配對:

iex> %{name: "Sean"} = sean
%Example.User<name: "Sean", roles: [...], ...>

自 Elixir 1.8 開始,結構體加入自省(introspection)。 要了解這代表著什麼以及如何使用它,現在來檢查(inspect) sean 擷取:

iex> inspect(sean)
"%Example.User<name: \"Sean\", roles: [...], ...>"

將所有欄位都呈現,以這個例子是可以的,但是如果我們有一個不希望被包括的受保護欄位呢? 新增的 @derive 功能讓我們能夠實現這件事! 現在將範例更新, roles 將不再包括在輸出中:

defmodule Example.User do
  @derive {Inspect, only: [:name]}
  defstruct name: nil, roles: []
end

_註_:也可以使用 @derive {Inspect, except: [:roles]},它們是同樣的。

隨著更新後的模組就位,現在來看看 iex 中會發生什麼:

iex> sean = %Example.User<name: "Sean", roles: [...], ...>
%Example.User<name: "Sean", ...>
iex> inspect(sean)
"%Example.User<name: \"Sean\", ...>"

roles 已被排除在輸出中!

合成 (Composition)

我們已經知道如何創建模組和結構體,現在讓我們學習如何通過合成來加入已存在的功能。 Elixir 提供了多種不同的方式來與其他模組進行互動。

別名 (alias)

允許在模組名稱中使用別名;這在 Elixir 的程式碼中使用相當頻繁:

defmodule Sayings.Greetings do
  def basic(name), do: "Hi, #{name}"
end

defmodule Example do
  alias Sayings.Greetings

  def greeting(name), do: Greetings.basic(name)
end

# Without alias

defmodule Example do
  def greeting(name), do: Sayings.Greetings.basic(name)
end

如果兩個別名之間有衝突,或者我們只是想完全使用別名,我們可以使用 :as 選項:

defmodule Example do
  alias Sayings.Greetings, as: Hi

  def print_message(name), do: Hi.basic(name)
end

同時為多個模組套用別名是可行的:

defmodule Example do
  alias Sayings.{Greetings, Farewells}
end

導入 (import)

如果想導入函數而不是別名 (aliasing) 模組,可以使用 import

iex> last([1, 2, 3])
** (CompileError) iex:9: undefined function last/1
iex> import List
nil
iex> last([1, 2, 3])
3

篩選 (Filtering)

預設情況下,所有的函數和巨集都會被導入,但是我們可以使用 :only:except 選項進行篩選。

要導入特定的函數和巨集,我們必須提供一對 (pairs) 名稱/引數數目給 :only:except。 讓我們從只導入 last/1 函數開始:

iex> import List, only: [last: 1]
iex> first([1, 2, 3])
** (CompileError) iex:13: undefined function first/1
iex> last([1, 2, 3])
3

導入除了 last/1 之外的所有內容,並嘗試與之前相同的函數:

iex> import List, except: [last: 1]
nil
iex> first([1, 2, 3])
1
iex> last([1, 2, 3])
** (CompileError) iex:3: undefined function last/1

除了一對的名稱/引數數之外,還有兩個特殊的 atoms, :functions:macros 分別只導入函數和巨集:

import List, only: :functions
import List, only: :macros

請求 (require)

可以使用 require 來告訴 Elixir 將使用來自其他模組的巨集 (macros)。 與 import 的細微差異在於只允許呼用巨集,而非被指定模組的函數:

defmodule Example do
  require SuperMacros

  SuperMacros.do_stuff
end

如果我們試圖呼用一個尚未載入的巨集,Elixir 將會出現錯誤訊息。

呼用 (use)

使用 use 巨集,我們可以讓另一個模組修改我們目前模組的定義。 當我們在程式碼中呼用 use 時,實際上是呼用由所提供模組定義的 __using__/1 回呼函數。 巨集 __using__/1 的結果將成為我們模組定義的一部分。 為了更好地理解實際上是如何運作的,現在來看一個簡單的例子:

defmodule Hello do
  defmacro __using__(_opts) do
    quote do
      def hello(name), do: "Hi, #{name}"
    end
  end
end

這裡我們創設了一個 Hello 模組,它定義了 __using__/1 回呼函數,而我們在其內部同時定義了一個 hello/1 函數。 讓我們創設一個新的模組,以便我們可以試用我們的新程式碼:

defmodule Example do
  use Hello
end

如果在 IEx 中試用我們的程式碼,會看到 Example 模組上的 hello/1

iex> Example.hello("Sean")
"Hi, Sean"

這裡可以看到 useHello 上呼用 __using__/1 回呼函數,然後將執行後的程式碼加入到模組中。 我們已經展示了一個基本的例子,現在更新我們的程式碼來看看 __using__/1 如何支援其它選項。 我們將通過加入一個選項 greeting 來實做:

defmodule Hello do
  defmacro __using__(opts) do
    greeting = Keyword.get(opts, :greeting, "Hi")

    quote do
      def hello(name), do: unquote(greeting) <> ", " <> name
    end
  end
end

現在更新我們的 Example 模組以包含新創設的選項 greeting

defmodule Example do
  use Hello, greeting: "Hola"
end

如果現在我們在 IEx 試用,應該會看到 greeting 已經改變了:

iex> Example.hello("Sean")
"Hola, Sean"

這些簡單的例子說明了 use 是如何運作的,它是 Elixir 工具箱中一個非常強大的工具。 當你繼續學習 Elixir 的時候,留意一下 use ,你肯定會看到一個例子,就是 use ExUnit.Case, async: true

quote, alias, use, require 是我們使用 metaprogramming 時引用的巨集。

Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!