07/09/2018, 15:48

Elixir cho dân Ruby - Phần 2

Bỏ quả phần 1? Xem phần 1 Trong phần này tôi sẽ giới thiệu về cách cấu trúc code trong Elixir, bắt đầu với biến (variable) và module. Để giúp các bạn độc giả Ruby tôi cũng sẽ cung cấp các ví dụ so sánh với các chức năng tượng tự có trên Ruby. Xin lưu ý, toàn bộ code sẽ được đánh thẳng vào iex ...

Bỏ quả phần 1? Xem phần 1

Trong phần này tôi sẽ giới thiệu về cách cấu trúc code trong Elixir, bắt đầu với biến (variable) và module. Để giúp các bạn độc giả Ruby tôi cũng sẽ cung cấp các ví dụ so sánh với các chức năng tượng tự có trên Ruby.

Xin lưu ý, toàn bộ code sẽ được đánh thẳng vào iex nên các bạn đọc giả nên mở sẵn iex.

Elixir là một ngôn ngữ động giống Ruby, không cần thiết chỉ định loại cấu trúc của biến, ngôn ngữ sẽ tự động kiểm tra và gán đúng kiểu. Lấy một ví dụ sau:

iex(1)> name = "Lệ Rơi"
"Lệ Rơi"

Ví dụ ở trên chúng ta gán một chuỗi vào biến name, trong giới Elixir thì bước gán này gọi là binding (hay dịch vui ra là 'đóng bìa').

Giao ước đặt tên cho biến, hàm và module

Vì được thiết kế với cảm hứng từ Ruby, giao ước hợp lệ tên các biến và hàm là kiểu snake_case (kiểu rắn) thay vì kiểu camelCase (kiểu lạc đà), là các từ khoá bắt đầu với ký tự không viết hoa:

ten_bien
bienHopLeNhungKhongKhuyenKhich

Không dừng ở đó, các biến và tên hàm có thể kết thúc với dấu ? và dấu !, (chà chà, sao mà giống Ruby thế nhỉ). Biến kết thúc ? vd sieu_sao? có giao ước là bạn nên trả về giá trị boolean true hay false, và với biến kết thúc !, vd hat_len_nao! thì sẽ quăng exception (cái này thì giống thông ước trong Rails).

Các từ khoá viết hoa đầu được dành để thể hiện module, vd:

TenMotModule

Dữ liệu không bị đột biến

Khi chúng ta gán một giá trị mới vào một biến:

iex(1)> name = "Lệ Rơi"
"Lệ Rơi"

iex(1)> name = "Thím Đàm"
"Thím Đàm"

Điều xảy ra ở phía sau là biến name sẽ trỏ vào một địa chỉ trên bộ nhớ khác. Lưu ý là elixir sẽ không thay đổi giá trị của địa chỉ bộ nhớ này. Thế nên chúng ta có thể thấy rằng biến có thể bị đột biến nhưng cái dữ liệu mà cá biến trỏ tới thì không bị hiệu ứng phụ này.

Module là gì? Module là một tập hợp hàm (functions), và cũng được dùng để làm namespace. Nghe quen nhỉ, vâng, đúng thế khái niệm này y hệt Ruby. Trong Ruby chúng ta khai báo module như sau:

module TongHopCacHitCuaLeRoi

  def hat_cho_nhau_nghe
    "Ola lal la alalaaalalalallala"
  end

  # namespacing
  module NhacVang
    def bai_mot
      "hat qua do"
    end
  end
end

với elixir thì module được định nghĩa với macro defmodule như sau:

defmodule TongHopCacHitCuaLeRoi do

  def hat_cho_nhau_nghe do
    "Ola lal la alalaaalalalallala"
  end

  # namespacing
  defmodule NhacVang do
    def bai_mot do do
      "hat qua do"
    end
  end
end

Thế khác biệt giữa hai ngôn ngữ là gì? Đó là module trong Ruby thường được dùng như là mixin và được nhúng vào class nào cần nó, vd:

class HotShow
  include TongHopCacHitCuaLeRoi
end

HotShow.new.hat_cho_nhau_nghe
# => "Ola lal la alalaaalalalallala"

Elixir là ngôn ngữ hướng chức năng nên khái niệm tạo instance object từ một class sẽ không có, thay vào đó các hàm sẽ được gọi với caller là module:

iex(1)> TongHopCacHitCuaLeRoi.hat_cho_nhau_nghe
iex(1)> TongHopCacHitCuaLeRoi.NhacVang.bai_mot

có thể thấy nó giống Ruby module functions:

module TongHopCacHitCuaLeRoi

  def self.hat_cho_nhau_nghe
    "Ola lal la alalaaalalalallala"
  end

  # namespacing
  module NhacVang
    def self.bai_mot
      "hat qua do"
    end
  end
end

TongHopCacHitCuaLeRoi.hat_cho_nhau_nghe
TongHopCacHitCuaLeRoi::NhacVang.bai_mot

Xin lưu ý là các bạn có thể khai báo namespace theo cách lặp lại namespace như sau:

defmodule TongHopCacHitCuaLeRoi do

  def hat_cho_nhau_nghe do
    "Ola lal la alalaaalalalallala"
  end

end

def TongHopCacHitCuaLeRoi.NhacVang do
  def bai_mot do
    "hat qua do"
  end
end

Hàm là container chứa logic của chương trình và hàm bắt buộc phải nằm trong module. Để khai báo hàm, chúng ta dùng macro def:

defmodule TenModule do
  def ten_ham(param1, param2) do
    "Noi dung"
  end
end

Hàm có thể có một parameter hoặc nhiều parameter ngăn cách bằng dấu phẩy ,.

Các bạn để ý từ lúc đầu, tôi dùng từ macro nhiều. Tối sẽ nói thêm về macro trong các phần sau. Macro giúp mở rộng thêm các cú pháp cho ngôn ngữ này, đây là ưu điểm đáng lưu ý.

Nếu nội dung của hàm rất ngắn, chúng ta có thể dùng kiểu cú pháp inline sau:

defmodule TenModule do
  def ten_ham(param1, param2); do: "Noi dung"
end

Cách gọi hàm cũng tương tự như gọi Ruby module function:

iex(1)> TenModule.ten_ham("hello")

và bất ngờ là việc wrap param với () không bắt buộc:

iex(1)> TenModule.ten_ham "hello"

Pipeline, tạm dịch thoáng ra là "dây truyền", là một khái niệm không mới. Khái niệm này có thể được thấy rất nhiều trong UNIX với chức năng pipeline (STDIN, STDOUT), hay với NodeJS Stream. Khái niệm này qui định rằng dữ liệu đầu vào sẽ đi qua một chuỗi các bước xử lý với kết quả của từng gia đoạn truyền tiếp cho bước tiếp theo đó, có thể tưởng tượng như bạn đang ở trong một qui trình làm mì gói trong nhà máy vậy, bột sẽ được tạo thành hỗn hợp rồi đem đi chiên và rồi đóng gói rồi cho vào xe chở đi phân phối.

Khái niệm pipeline được sử dụng phổ biến trong tất cả ngôn ngữ hướng đối tượng, trong Elixir thì pipeline được thể hiện qua operator |>. Hãy cùng xem xét ví dụ sau:

iex(1)> "Lệ Rơi" |> String.upcase |> IO.puts
LỆ RƠI

mã ở trên sẽ được biên dịch thành mã tương đương sau:

IO.puts(String.upcase("Lệ Rơi"))

có thể thấy được rằng kết quả trả về sẽ được cung cấp vào là parameter đầu tiên của hàm tiếp theo, trong ví dụ này thì chuỗi trả về "Lệ Rơi" sẽ được đưa vào param đầu tiên của hàm String.upcase và kết quả của String.upcase sẽ được đưa vào param đầu tiên của hàm IO.puts.

Hãy cùng xem một ví dụ khác:

truoc(param1, param2) |> sau(params3, param4)

sẽ được biên dịch thành:

sau(truoc(param1, param2), params3, param4)

Các bạn nên lưu nhớ đặc điểm về param 1 sẽ nhận tiếp giá trị trả về.

Pipeline có thể được thể hiện trên nhiều dòng để tăng tính dễ nhìn:

"Lệ Rơi"
|> String.upcase
|> IO.puts

Xin lưu ý là code trên không chạy với iex, bạn phải cho nó vào một file mới được.

Giao ước đặt tên của Elixir rất giống Ruby với tên biến và tên hàm tuân theo kiểu snake_case và tên module sẽ có ký tự đầu viết hoa giống kiểu Ruby class.

Module là tổ hợp các hàm và được sử dụng trực tiếp, không có khái niệm tạo instance
object hay mixin vào class như của Ruby.

Từ khoá |> dùng để xâu chuỗi nhiều hàm thành một pipeline, với giá trị của một hàm được trả về và cho thẳng vào param số 1 của hàm tiếp theo.

Trong bài viết sau tôi sẽ giới thiệu tiếp về function arity.

Đọc tiếp phần 3

0