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

Đặc tả và kiểu

Trong bài học này, chúng ta sẽ học về cú pháp @spec@type. @spec giống như là một cú pháp hỗ trợ để viết tài liệu, và có thể được phân tích bởi các công cụ khác, @type giúp chúng ta viết các code dễ đọc và dễ hiểu hơn.

Giới thiệu

Thông thường, bạn sẽ muốn mô tả giao diện cho hàm của bạn. Tất nhiện bạn có thể sử dụng @doc annotation, tuy nhiên nó chỉ là các thông tin cho các lập trình viên khác, mà không được kiểm tra trong lúc biên dịch. Cho mục đích này, Elixir cung cấp @spec annotation để mô tả các đặc tả của hàm sẽ được kiểm tra bởi trình biên dịch

Tuy nhiên, trong một số trường hợp, các đặc tả sẽ trở nên khá lớn và phức tạp. Nếu bạn muốn giảm bớt tính phức tạp, nhưng vẫn muốn đưa ra định nghĩa cho các kiểu mới, Elixir cung cấp anotiation(tạm dịch là ký tự chú thích)@type để làm việc này. Nói cách khác, Elixir vẫn là một ngôn ngữ kiểu động (dynamic language). Nghĩa là tất cả các thông tin về kiểu sẽ bị trình biên dịch bỏ qua, nhưng nó có thể được sử dụng bởi các công cụ khác.

Đặc tả

Nếu đã có kinh nghiệm với Java, bạn có thể coi đặc tả như là một interface. Đặc tả định nghĩa kiểu của các tham số của hàm, cũng như kiểu của giá trị trả về.

Để định nghĩa kiểu đầu vào và đầu ra, chúng ta sử dụng @spec ngay trước định nghĩa hàm, các tham số cho @spec sẽ là tên của hàm, danh sách kiểu của các tham số cho hàm đó, tiếp theo là ::, cuối cùng là kiểu của giá trị trả về

Hãy cùng xem ví dụ dưới đây:

@spec sum_product(integer) :: integer
def sum_product(a) do
  [1, 2, 3]
  |> Enum.map(fn el -> el * a end)
  |> Enum.sum()
end

Mọi thứ trông vẫn tốt, và khi chúng ta gọi hàm này, một giá trị hợp lệ sẽ được trả về, nhưng hàm Enum.sum trả về number chứ không phải là integer như chúng ta mong muốn ở trong @spec. Đây có thể nguồn gốc của các lỗi! Có những công cụ như Dialyzer để phân tích tĩnh (static analysis) code có thể giúp chúng ta tìm những lỗi kiểu này. Chúng ta sẽ nói về chúng trong một bài học khác.

Kiểu tuỳ biến

Các đặc tả là rất tốt, tuy nhiên đôi khi các hàm của chúng ta làm việc với nhiều cấu trúc dữ liệu phức tạp hơn là các số hoặc là các tập hợp (collections). Trong trường hợp này, @spec có thể sẽ trở nên rất khó hiểu, hoặc khó thay đổi đối với các lập trình viên khác. Đôi khi các hàm cần nhận vào một lượng lớn các tham số, hoặc là trả về một dữ liệu phức tạp. Một danh sách dài các tham số có thể là một trong những chỗ “bốc mùi” (code smell) trong code. Trong các ngôn ngữ hướng đối tượng giống như Ruby và Java, chúng ta có thể dễ dàng định nghĩa các class để giải quyết vấn đề này. Elixir không có class những bởi vì nó rất dễ để mở rộng, chúng ta có thể định nghĩa kiểu của riêng chúng ta.

Ngoài ra, Elixir chứa một vài kiểu cơ bản như integer, hoặc là pid. Bạn có thể tìm hiểu về danh sách các kiểu có sẵn của Elixir trong tài liệu.

Định nghĩa kiểu tuỳ biến

Hãy cùng thay đổi hàm sum_times và giới thiệu thêm một vài tham số mới:

@spec sum_times(integer, %Examples{first: integer, last: integer}) :: integer
def sum_times(a, params) do
  for i <- params.first..params.last do
    i
  end
  |> Enum.map(fn el -> el * a end)
  |> Enum.sum()
  |> round
end

Chúng ta giới thiệu thêm một struct trong Examples module, chứa hai trường firstlast. Đây là phiên bản đơn giản hơn của module Range. Chúng ta sẽ nói về struct khi chúng ta thảo luận về modules. Tưởng tượng rằng, chúng ta muốn mô tả đặc tả với Examples struct trong rất nhiều chỗ. Những đặc tả này có thể sẽ rất dài, phức tạp, và có thể là cội nguồn của các bug. Một giải pháp cho chuyện này là dùng @type.

Elixir cung cấp ba cách dùng để định nghĩa kiểu:

Hãy cùng định nghĩa kiểu của chúng ta:

defmodule Examples do
  defstruct first: nil, last: nil

  @type t(first, last) :: %Examples{first: first, last: last}

  @type t :: %Examples{first: integer, last: integer}
end

Chúng ta định nghĩa kiểu t(first, last) để đại diện cho struct %Examples{first: first, last: last}. Lúc này, chúng ta thấy các kiểu có thể nhận vào các parameter, nhưng chúng ta cũng định nghĩa kiểu t, và lúc này nó đại diện cho struct %Examples{first: integer, last: integer}.

Điểm khác nhau là gì? Cái đầu tiên đại diện cho Examples struct mà hai khoá có thể là bất cứ kiểu này. Cái thứ hai đại diện cho struct, trong đó các khoá là các integer. Điều này có nghĩa là đoạn code sau:

@spec sum_times(integer, Examples.t()) :: integer
def sum_times(a, params) do
  for i <- params.first..params.last do
    i
  end
  |> Enum.map(fn el -> el * a end)
  |> Enum.sum()
  |> round
end

Tương đương với:

@spec sum_times(integer, Examples.t(integer, integer)) :: integer
def sum_times(a, params) do
  for i <- params.first..params.last do
    i
  end
  |> Enum.map(fn el -> el * a end)
  |> Enum.sum()
  |> round
end

Viết tài liệu cho kiểu

Thành phần cuối cùng chúng ta muốn thảo luận là về các để viết tài liệu cho kiểu. Như chúng ta đã biết từ bài documentation, chúng ta có @doc@moduledoc để viết các tài liệu cho hàm và cho module. Để viết tài liệu cho kiểu, chúng ta có thể dùng @typedoc:

defmodule Examples do
  @typedoc """
      Type that represents Examples struct with :first as integer and :last as integer.
  """
  @type t :: %Examples{first: integer, last: integer}
end

@typedoc là tương tự @doc and @moduledoc.

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