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

Modules

Chúng ta biết từ kinh nghiệm là để tất cả các hàm vào trong một file khá là không tốt. Trong bài học này, chúng ta sẽ học cách nhóm các hàm lại với nhau, và định nghĩa một loại map đặc biệt là struct để tổ chức code một cách hiệu quả hơn.

Modules

Module là cách tốt nhất để tổ chức các hàm vào một namespace. Ngoài việc nhóm các hàm với nhau, nó còn cho phép chúng ta định nghĩa các “named function” public và private như chúng ta đã học ở bài học trước.

Hãy cùng xem một ví dụ cơ bản:

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

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

Trong Elixir, chúng ta có thể tạo các module lòng này, điều này cho phép bạn có thể dễ dàng phân chia các tính năng hơn.

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."

Thuộc tính của module

Thuộc tính của module phần lớn được dùng như hằng số trong Elixir. Hãy cùng xem một ví dụ đơn giản:

defmodule Example do
  @greeting "Hello"

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

Cần đặc biệt chú ý rằng: có một vài thuộc tính đặc biệt trong Elixir. Ba thuộc tính phổ thông nhất là:

Structs

Struct là các map đặc biệt được định nghĩa như một tập các khoá và các giá trị mặc định. Một struct phải được định nghĩa trong một module, từ đó tên của struct sẽ là tên của module. Rất là bình thường nếu như struct là thứ duy nhất mà module định nghĩa trong nó.

Để định nghĩa một struct, chúng ta sử dụng macro defstruct cùng với một keyword list của các trường và các giá trị mặc định:

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

Hãy cùng tạo một vài structs:

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]>

Chúng ta cũng có thể cập nhật struct giống như chúng ta làm với một map:

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

Quan trọng nhất, chúng ta có thể so trùng mẫu struct với map:

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

Composition

Giờ chúng ta đã biết cách để tạo các module và struct. Hãy cùng học cách để thêm cách tính năng đã tồn tại vào trong chúng thông qua composition. Elixir cung cấp cho chúng ta một vài cách để tương tác giữa các module với nhau.

alias

Cho phép chúng ta có thể “alias” tên của một module, “alias” được sử dụng khá thường xuyên trong 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

Nếu như có xung đột giữa 2 alias, hoặc chúng ta muốn alias với một tên hoàn toàn khác, chúng ta có thể sử dụng lựa chọn :as:

defmodule Example do
  alias Sayings.Greetings, as: Hi

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

Chúng ta thậm chí có thể alias nhiều module cùng một lúc:

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

import

Nếu chúng ta muốn import nhiều hàm và macros thay vì alias module, chúng ta có thể dùng import/

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

Filtering

Mặc định tất cả các hàm và macro sẽ được import vào, nhưng chúng ta có thể lọc chúng ra bằng cách sử dung lựa chọn :only:except.

Để import các hàm và macro cụ thể, chúng ta sẽ phải cung cấp một cặp tên/số tham số (arity) cho :only:except. Hãy cùng import chỉ nguyên hàm 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

Nếu chúng ta import tất cả mọi thứ trừ last/1, chúng ta có thể làm như sau:

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

Bên cạnh các cặp tên/arity, có 2 atom đặc biệt, :functions:macros được dùng để chỉ import các hàm hoặc các macro tương ứng:

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

require

Mặc dùng ít khi sử dụng, require/2 dù sao cũng khá quan trong để yêu cầu một module đảm bảo rằng nó được biên dịch vào nạp vào. Điều này đặc biệt hữu dụng nếu chúng ta muốn truy cập vào các macro của một module:

defmodule Example do
  require SuperMacros

  SuperMacros.do_stuff
end

Nếu chúng ta có gắng gọi một macro chưa được nạp trong Elixir, một lỗi sẽ bị văng ra.

use

use macro sẽ gọi tới một macro đặc biệt, được gọi là __using__/1 từ module được chỉ định. Đây là một ví dụ:

# lib/use_import_require/use_me.ex
defmodule UseImportRequire.UseMe do
  defmacro __using__(_) do
    quote do
      def use_test do
        IO.puts("use_test")
      end
    end
  end
end

và chúng ta có thể thêm dòng này vào trong UseImportRequire

use UseImportRequire.UseMe

sử dụng module UseImportRequire.UseMe để định nghĩa một hàm use_test/0 thông qua việc gọi tới macro __using__/1.

Đó là tất cả những gì mà use làm. Ngoài ra, __using__ được sử dụng khá là phổ biến để gọi tới các alias, requireimport. Những macro này sẽ tạo các alias, hoặc là import trong module đươc sử dụng. Điều này cho phép module có thể được sử dụng để định nghĩa một chính sách để các hàm các các macros có thể tham chiếu lẫn nhau. Nó khá là linh động khi __using__/1 có thể được sử dụng để cấu hình việc tham chiếu tới các module khác, đặc biệt là các module con (submodule).

Phoenix framework sử dụng __using__/1 để giảm bớt việc phải lặp lại các alias và import call trong các module do lập trình viên định nghĩa.

Sau đây là một ví dụ rất ngắn gọi từ trong module Ecto.Migration:

defmacro __using__(_) do
  quote location: :keep do
    import Ecto.Migration
    @disable_ddl_transaction false
    @before_compile Ecto.Migration
  end
end

Macro Ecto.Migration.__using__/1 bao gồm một lời gọi import, do vậy khi bạn gọi use Ecto.Migration, bạn cũng gọi tới import Ecto.Migration. Nó cũng cấu hình một thuộc tính của module từ đó chúng ta sẽ điều khiển hoạt động của Ecto.

Nói tóm lại: use macro đơn giản gọi tới __using__/1 macro của module cụ thể. Để thực sử hiều nó làm những gì, bạn cần đọc vào module __using__/1

Note: quote, alias, use, require là các macro được sử dụng khi chúng ta làm việc với metaprogramming.

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