Giới thiệu về Pattern Matching trong Elixir
elixir
25
White

kiennt viết ngày 16/04/2016

Bài viết được dịch từ bài viết gốc trên elixirvn.com

image

Pattern maching không phải là một tính năng mới chỉ có ở Elixir. Tính năng này đã được đưa vào các ngôn ngữ lập trình hàm khác từ lâu. Khi mới bắt đầu học về lập trình hàm (thông qua khoá học về Scala trên Courera), khái niệm pattern matching khá lạ lẫm với tôi. Với Elixir, pattern matching đã trở nên rõ ràng. Trong bài viết này, tôi muốn chia sẻ với các bạn những gì mình học được về Pattern matching.

Toán tử match

Trong Elixir, toán tử = không có nghĩa là "gán giá trị của biến này tới biến khác". Thực tế, toán tử này có ý nghĩa "match" biến bên trái với biểu thức bên phải. Thông thường, bạn có thể sử dụng nó như là toán tử gán trong các ngôn ngữ lập trình khác. Tuy nhiên, ở một vài cases, bạn sẽ nhận thấy sự khác biệt.

language = "elixir"
"elixir" = language
"ruby" = language    # => raise MatchError

Đầu tiên, ta "match" language với "elixir". Bởi vì đây là toán tử match, nên sau dòng đầu tiên, bạn cũng có thể "match" "elixir" với biên language. Cả 2 statements này đều là đúng. Tuy nhiên, giờ nếu bạn match "ruby" với language, thì sẽ có lỗi MatchError xảy ra.

Sử dụng lại biến

Elixir là ngôn ngữ lập trình hàm, tất cả các biến trong Elixir là immutable. Có một sự khác biệt giữa elixir và các ngôn ngữ lập trình hàm khác đó là bạn có thể sử dụng lại biến trong Elixir. Nếu bạn match language với "elixir", sau đó sử dụng lại biến language để match nó với "ruby", giá trị của language giờ sẽ là "ruby"

language = "elixir"
language = "ruby"   # re-match value of language
"ruby" = language

Ẩn bên dưới máy ảo của Erlang, vẫn còn một phiên bản của biến language được gán với "elixir".
Cụ thể, nếu trước khi match language với "ruby", bạn truyền biến này tới một process khác chạy song song, rồi mới match lại language, thì process đó vẫn sẽ coi giá trị của language"elixir" chứ không phải là "ruby"

Toán tử pin

Nếu bạn muốn match giá trị của biến hiện tại với một giá trị khác thì sao? Elixir hỗ trợ toán tử pin để làm chuyện này.

language = "elixir"
^language = "ruby"   # raise MatchError

Chú ý ký tự ^ ở đây. Ký tự này chính là toán tử pin, nó có ý nghĩa là, lấy giá trị của biến language và match với "ruby". Do trước đó language đã được match với "elixir", nên ở đây toán tử match này sẽ bị lỗi.

Pattern matching với các kiểu dử liệu khác nhau

Như bạn đã thấy ở các ví dụ trên, bạn có thể match một biến với bất cứ kiểu dữ liệu này.
Ngoài việc match biến với một biến hoặc biểu thức khác, Elixir còn support việc destructuring một biến thành nhiều biến khác.

# Destructure tuple
first, second = {"hello", :world}
# first => "hello"
# second => :world

# Destructure list
[head | tail] = [1, 2, 3, 4, 5]
# head => 1
# tail => [2, 3, 4, 5]

# Destructure map
%{"name" => name, "creator" => creator} = %{"name" => "elixir", "creator" => "José Valim"}
# name => "elixir"
# creator => "José Valim"

defmodule Language do
  defstruct [:name, :creator]
end

language = %Language{name: "python", creator: "Guido Rossum"}
%Language{name: name} = language
# name => "python"

Cụ thể, bạn có thể destructure tuple, list, keywork list và map. Elixir còn support destructure Struct do user tự định nghĩa. Do bản thân struct được implement bởi map trong Elixir, nên việc destruct Struct là giống như destructure map.

Matching binary

Một trong những thú vị của pattern matching đó là matching binary. Theo tôi, có lẽ Elixir là ngôn ngữ mạnh nhất về việc pattern matching binary.
Trong Elixir, để biểu diễn một số binary chúng ta sử dụng syntax <<A>> trong đó A là một byte - một số nguyên trong khoảng tử 0 đến 255. Nếu muốn định nghĩa một chuỗi các bytes, giả sử một số 2 bytes, ta sử dụng syntax <<1, 2>>. Với cách này ta đã tạo ra một chuỗi nhị phân 00000001 00000010

Matching binary giống với matching list, nhưng có nhiều tính năng hơn. Bạn có thể match để lấy ra byte đầu tiên, 3 bytes kế tiếp, rồi phần còn lại của chuỗi bytes. Bạn thậm chí còn có thể match số lượng byte với size là một biến được truyền vài

<<a, b::binary-size(3), rest::binary>> = <<1, 2, 3, 4, 5>>
# a => 1
# b => <<2, 3, 4>>
# rest => <<5>>

size = 2
<<a, b::binary-size(size), rest::binary>> = <<1, 2, 3, 4, 5>>
# a => 1
# b => <<2, 3>>
# rest => <<4, 5>>

Nếu thay vì lấy 3 bytes tiếp theo, bạn muốn lấy 3 bits tiếp theo, thì bạn sử dụng syntax ::size thay thế cho `::binary-size

<<a, b::size(3), rest::bitstring>> = <<1, 2, 3, 4, 5>>
# a => 1
# b => 0
# rest => <16, 24, 32, 5::size(5)>>

Giờ b sẽ nhận giá trị là 3 bits tiếp theo, do đó phần còn lại sẽ là một chuỗi 29 bits. Chuỗi này không thể nằm trong một chuỗi các bytes, do đó ta phải set kiểu của chuỗi rest là bitstring thay vì binary

Với việc hỗ trợ matching bitstring, Elixir cho phép bạn lấy ra một chuỗi các bit. Điều này đặc biệt hữu dụng khi bạn việc các thư viện để làm việc với các binary protocol. Khi tôi viết thư viện để parse HTTP2 protocal, tính năng này của Elixir giúp tôi giảm bớt rất nhiều đoạn code thừa.

image

Matching function parameters

Khi gọi một hàm, dựa vào các tham số truyền vào hàm, Elixir sẽ match để gọi ra đúng hàm mà chúng ta muốn.
Hãy xem ví dụ khi tôi implement hàm tính số fibonacci thứ n.

defmodule WithoutFunctionMatching do
  def fibo(n) do
    if n == 0, do: 1
    else if n == 1, do: 1
    else, do: fibo(n - 1) + fibo(n - 2)
  end
end

defmodule WithFunctionMatching do
  def fibo(0), do: 1
  def fibo(1), do: 1
  def fibo(n), do: fibo(n - 1) + fibo(n - 2)
end

Ví dụ trên, tôi đưa ra 2 cách để cài đặt hàm fibo. Nếu không sử dụng pattern matching, chúng ta phải sử dụng if/else trong thân hàm. Bằng cách sử dụng pattern matching, chúng ta loại bỏ việc dùng if/else. Và điều này theo tôi làm cho code trở nên sang sủa và dể hiểu hơn.
Không chỉ có tác dụng khi đọc code, pattern matching còn làm cho việc gọi hàm trở nên nhanh hơn. Nguyên nhân là vì máy ảo Erlang có khả năng build một cây nhị phân các hàm thay vì việc kiểm tra tuyến tính độ match của parameter. Chinh cây nhị phân này giúp cho việc tìm kiếm hàm gọi nhanh hơn.

For you to see any benefits, you would need a good number of tests because at the end of the day it boils down to the fact conditional checking is linear (O(N)) while patterns may be optimized to a binary tree search (O(log2N))
José Valim

Tôi đã thử viết một test để kiểm tra performance của việc gọi hàm bằng pattern matching. Kết quả khá ấn tượng

defmodule Foo do
  def cc(?0), do: 0
  def cc(?1), do: 1
  def cc(?2), do: 2
  def cc(?3), do: 3
  def cc(?4), do: 4
  def cc(?5), do: 5
  def cc(?6), do: 6
  def cc(?7), do: 7
  def cc(?8), do: 8
  def cc(?9), do: 9

  def cc_with_map(value) do
    %{
      ?0 => 0,
      ?1 => 1,
      ?2 => 2,
      ?3 => 3,
      ?4 => 4,
      ?5 => 5,
      ?6 => 6,
      ?7 => 7,
      ?8 => 8,
      ?9 => 9,
    }[value]
  end

  @maps %{
      ?0 => 0,
      ?1 => 1,
      ?2 => 2,
      ?3 => 3,
      ?4 => 4,
      ?5 => 5,
      ?6 => 6,
      ?7 => 7,
      ?8 => 8,
      ?9 => 9,
  }
  def cc_with_map_of_module(value) do
    @maps[value]
  end
end

defmodule PatternMatchingBench do
  use Benchfella

  bench "pattern matching" do
    1..10000 |> Enum.each(fn(_) -> Foo.cc(?9) end)
  end

  bench "maps" do
    1..10000 |> Enum.each(fn(_) -> Foo.cc_with_map(?9) end)
  end

  bench "module maps" do
    1..10000 |> Enum.each(fn(_) -> Foo.cc_with_map_of_module(?9) end)
  end
end
$ mix bench
Settings:
  duration:      1.0 s

## PatternMatchingBench
[23:20:11] 1/3: pattern matching
[23:20:13] 2/3: module maps
[23:20:15] 3/3: maps

Finished in 6.07 seconds

## PatternMatchingBench
pattern matching        2000   867.41 µs/op
module maps             1000   1470.22 µs/op
maps                    1000   1513.29 µs/op

Matching with guard

Quay trở lại với cách tôi implement hàm fibo ở trên. Có một bug ở đây, nếu tôi truyền vào giá trị n với n < 0, trong trường hợp này hàm fibo(n) sẽ không giờ dừng.
Để giải quyết vấn đề này, ta có thể sử dụng guarding trong Elixir

defmodule FobiWithGuard do
  def fibo(0), do: 1
  def fibo(1), do: 1
  def fibo(n) when n > 1, do: fibo(n - 1) + fibo(n - 2)
end

Bằng cách thêm một biểu thức guard when n > 1 ở dòng thứ 4, tôi có thể đảm bảo là hàm này chỉ được gọi nếu tham số n > 1.

Biểu thức guard là một công cụ rất tốt khi bạn muốn giới hạn chặt chẽ các tham số đầu vào. Tuy nhiên, một lưu ý khi dùng biểu thức guard đó là máy ảo Erlang không thể tối ưu (bằng cách sinh cây nhị phân) cho các hàm với biểu thức guard


Pattern matching là một trong những khái niệm cốt lõi của Elixir. Với bài viết này, tôi hy vọng đã giúp các bạn hiểu hơn về khái niệm này trong Elixir. Các bài viết tiếp theo sẽ trình bày thêm một số kỹ thuật để sử dụng pattern matching khi lập trình.

Bài viết được dịch từ bài viết gốc trên elixirvn.com

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

kiennt

29 bài viết.
230 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
82 18
Mọi chuyện bắt đầu từ nắm 2013 trong quá trình xây dựng chức năng login với Facebook, tôi đã tìm ra một cách để tấn công vào các hệ thống login với...
kiennt viết gần 2 năm trước
82 18
White
26 5
1. Đặt vấn đề Một trong các vấn đề của một hệ thống backend là bài toán điều phối request tới các nguồn dữ liệu. Xét bài toán với một hệ thống bl...
kiennt viết hơn 1 năm trước
26 5
White
22 4
Mở đầu Tôi đang học về (Link) (Link) (hệ thống phân tán). Đây là một lĩnh vực rất hay và khó (đối với tôi). Phần lớn các hệ thống máy tính hiện na...
kiennt viết hơn 1 năm trước
22 4
Bài viết liên quan
White
8 6
Chưa xem phần 2? Xem (Link) Trong bài viết này tôi giới thiệu cho các bạn về khái niệm function arity, một cách gọi mĩ miều của số lượng argument ...
Lơi Rệ viết hơn 2 năm trước
8 6
White
0 0
Custom Ecto.Type Version hiện tại của Ecto.Type không support một số datatype sử dụng khi validate. Ví dụ như MapSet. Thành ra đành phải tự viết đ...
Vie viết 8 tháng trước
0 0
White
0 1
Tìm nhanh package trên (Link) Thanh công cụ tìm kiếm của (Link) có hỗ trợ một số tham số để hỗ trợ tìm chính xác hơn. name: Tìm kiếm chính xác t...
Cẩm Huỳnh viết 7 tháng trước
0 1
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
29 bài viết.
230 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!