Học Elixir trong một giấc mơ

[BETA]

E lík xơ - tác giả José Valim đọc tại đây.

Lý do bạn thích học Haskell, LISP, Erlang, Elixir, Ocaml, Scala... những ngôn ngữ lập trình hàm (functional programming language) có vẻ hay ho, mà không bao giờ thành công là gì?

Sau đây là vài lý do mình gặp phải:

Để khắc phục điều này, ta sẽ:

Nếu không có gì để làm, nuốt xong các khái niệm, 1 tuần sau bạn sẽ lại quên.

Elixir/Erlang hoàn toàn đủ khả năng để cho bạn lập trình loanh quanh mấy thuật toán, giải các bài toán / vấn đề trên HackerRank. Nhưng điểm sáng của ngôn ngữ này, thực ra chỉ toả sáng khi ta dùng nó để phát triển các hệ thống lớn, cần chạy phân tán, hay chạy song song... Ít khi một người sẽ làm hẳn một project lớn như vậy. Vì vậy, ta thường không có đất dùng cho Elixir hay Erlang.

Một cách ứng dụng để chơi với ngôn ngữ mới nữa "nhỏ hơn", là viết các câu lệnh thực hiện một việc gì đó (CLI). Vậy nhưng Elixir/Erlang không toả sáng/ đơn giản trong công việc này, nó CÓ THỂ làm được, nhưng trên thực tế không mấy ai làm.

Ví dụ standard khi học Erlang là làm một hệ thống chat. Trên thực tế, hệ thống chat của "Whatsapp" được viết bằng Erlang hệ thống này đã được bán lại cho FaceBook với giá 19 TỶ đô la Mỹ (PS: FaceBook trước đó cũng đã mua lại Instagram - một hệ thống viết bằng Django/Python với giá 1 TỶ đô la Mỹ)

Ta có thể làm 1 website sau khi học Elixir, đây là lĩnh vực hiện tại mà Elixir mạnh nhất.

Đã xong phần ý tưởng, bây giờ hãy học Elixir.

Thực hiện trên:

$ elixir --version
Erlang/OTP 20 [erts-9.0] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]

Elixir 1.5.0

Bật iex:

$ iex
Erlang/OTP 20 [erts-9.0] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]

Interactive Elixir (1.5.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> IO.puts "Hello FAMILUG.org!"
Hello FAMILUG.org!
:ok

PS: trong bài có một số đoạn so sánh với tính năng tương tự trên Python - bởi tác giả là người có kinh nghiệm với Python - giúp các lập trình viên Python dễ kết nối các khái niệm. Phần so sánh này hoàn toàn không cần thiết với người không biết Python và có thể bỏ qua.

Những điều cần biết

Các ví dụ trong bài này đều thực hiện trực tiếp trên iex

cài Elixir xong sẽ có kèm iex luôn.

Hủy một câu lệnh gõ hỏng

Khi gõ một sai một câu lệnh, thực hiện bước sau để hủy:

iex(5)>
User switch command
--> i
--> c
** (EXIT) interrupted

Tức bấm Ctrl-G rồi i, rồi c.

Hoặc cách khác là gõ #iex:break

iex(5)> IO.put(
...(5)> #iex:break
** (TokenMissingError) iex:5: incomplete expression

Dùng IO.inspect(x) để in ra màn hình (để debug) thay vì IO.puts(x) (tương tự print trong các ngôn ngữ khác).

Xóa màn hình

clear để xóa sạch màn hình (gọi function clear).

Các kiểu dữ liệu cơ bản

Integer

iex(33)> 42
42
iex(34)> i 42
Term
  42
Data type
  Integer
Reference modules
  Integer

Dùng ``i OBJECT`` để hiển thị thông tin về giá trị/ kiểu của object. Như "type(object)" trong Python hay "typeof object" trong JavaScript.

Float

iex(35)> i 3.14
Term
  3.14
Data type
  Float
Reference modules
  Float

iex(36)> 0.1 + 0.1 + 0.1
0.30000000000000004

Atom

iex(39)> i true
Term
  true
Data type
  Atom
Reference modules
  Atom
iex(40)> i false
Term
  false
Data type
  Atom
Reference modules
  Atom

String

iex(42)> i "Elixir"
Term
  "Elixir"
Data type
  BitString
Byte size
  6
Description
  This is a string: a UTF-8 encoded binary. It's printed surrounded by
  "double quotes" because all UTF-8 encoded codepoints in it are printable.
Raw representation
  <<69, 108, 105, 120, 105, 114>>
Reference modules
  String, :binary

Các kiểu dữ liệu chứa được kiểu khác: list, tuple, dict

List

iex(44)> i 'abc'
Term
  'abc'
Data type
  List
Description
  This is a list of integers that is printed as a sequence of characters
  delimited by single quotes because all the integers in it represent valid
  ASCII characters. Conventionally, such lists of integers are referred to as
  "charlists" (more precisely, a charlist is a list of Unicode codepoints,
  and ASCII is a subset of Unicode).
Raw representation
  [97, 98, 99]
Reference modules
  List


iex(45)> i [1,2,3,3.14]
Term
  [1, 2, 3, 3.14]
Data type
  List
Reference modules
  List

iex(46)> i []
Term
  []
Data type
  List
Reference modules
  List

Một list có thể chứa bất kỳ kiểu dữ liệu nào:

iex(6)> i ["abc", 1]
Term
  ["abc", 1]
Data type
  List
Reference modules
  List

Với list chứa các phần tử tuple-2 {atom: value}, Elixir hỗ trợ thêm cú pháp ngắn gọn để tạo ra list này và gọi là "keyword list" (bản chất vẫn là 1 list bình thường):

iex(38)> [{:name, "PyMi"}, {:est, 2015}]
[name: "PyMi", est: 2015]
iex(39)> [name: "PyMi", est: 2015]
[name: "PyMi", est: 2015]

iex(38)> [{:name, "PyMi"}, {:est, 2015}]
[name: "PyMi", est: 2015]
iex(39)> [name: "PyMi", est: 2015]
[name: "PyMi", est: 2015]

Có thể truy cập phần tử của keyword list bằng key, khi nhiều key trùng nhau, truy cập sẽ trả về giá trị đầu tiên ứng với key.

iex(16)> [foo: "pika", foo: "pikachu"]
[foo: "pika", foo: "pikachu"]
iex(17)> [foo: "pika", foo: "pikachu"][:foo]
"pika"

Tuple

iex(48)> i {100, 'abc'}
Term
  {100, 'abc'}
Data type
  Tuple
Reference modules
  Tuple
iex(49)> i {}
Term
  {}
Data type
  Tuple
Reference modules
  Tuple

Không giống Python, trong Elixir, TẤT CẢ các kiểu dữ liệu đều là immutable, tức một khi đã tạo ra, không thể thay đổi. Muốn "thay đổi", ta phải tạo mới.

Trên Python, những điều sau đều có thể làm trên list hay tuple: - duyệt qua từng phần tử (loop) - truy cập index - slice để thu được một tập con

Sự giống nhau về tính năng khiến người dùng thường hỏi khi nào dùng list, khi nào dùng tuple. Với Python, ta có thể thay đổi 1 list, nhưng không thể thay đổi một tuple sau khi đã tạo ra nó. Với Elixir, cả list và tuple đều không thể thay đổi được (immutable). Đặc điểm này sẽ giúp thấy rõ hơn khi nào dùng tuple và khi nào nên dùng list: - list THƯỜNG dùng để chứa các dữ liệu tương tự nhau (heterogenious) - tuple thường để chứa các thông tin liên quan đến nhau, như các cột trong 1 dòng của database, các toạ độ của một điểm, các đặc tính của một con mèo...

Khi đó, ta sẽ thường loop qua 1 list, và thường truy cập đến các phần tử của tuple thông qua indexing/unpacking.

iex(52)> person = {"HVN", 27, "Python"}
{"HVN", 27, "Python"}
iex(53)> {name, age, language} = person
{"HVN", 27, "Python"}
iex(54)> name
"HVN"

http://elixir-lang.org/getting-started/basic-types.html#lists-or-tuples http://stackoverflow.com/questions/31192923/lists-vs-tuples-what-to-use-and-when

Map (dictionary)

iex(51)> i %{"name": "FAMILUG"}
Term
  %{name: "FAMILUG"}
Data type
  Map
Reference modules
  Map

MapSet - kiểu dữ liệu tập hợp

Chứa mỗi phần tử 1 lần, không có thứ tự trừ 32 phần tử đầu (tức không thể sắp xếp, phải đổi thành kiểu list mới sắp xếp được.)

iex(2)> i MapSet.new([1,2,2,3,2,1])
Term
  #MapSet<[1, 2, 3]>
Data type
  MapSet
Description
  This is a struct. Structs are maps with a __struct__ key.
Reference modules
  MapSet, Map
Implemented protocols
  IEx.Info, Enumerable, Inspect, Collectable

Struct

Struct là dạng đặc biệt của Map, dùng để biểu diễn các kiểu dữ liệu do người dùng tự định nghĩa.

iex(1)> defmodule Person do
...(1)>   defstruct [:name, :age]
...(1)> end
{:module, Person,
 <<70, 79, 82, 49, 0, 0, 8, 20, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0,
   234, 0, 0, 0, 22, 13, 69, 108, 105, 120, 105, 114, 46, 80, 101, 114,
   115, 111, 110, 8, 95, 95, 105, 110, 102, 111, 95, 95, ...>>,
 %Person{age: nil, name: nil}}
iex(2)> i %Person{name: "HVN", age: 27}
Term
  %Person{age: 27, name: "HVN"}
Data type
  Person
Description
  This is a struct. Structs are maps with a __struct__ key.
Reference modules
  Person, Map
Implemented protocols
  IEx.Info, Inspect

Các thao tác cơ bản với các kiểu dữ liệu

Thao tác với số

iex(11)> :math.pow(2,4)
16.0
iex(12)> :math.sin(2 * :math.pi)
-2.4492935982947064e-16
iex(17)> :math.sqrt(4)
2.0
iex(24)> trunc 5.7
5
iex(25)> trunc 5.1
5
iex(26)> "5" |> String.to_integer
5
iex(28)> round 6.5
7
iex(29)> round 7.5
8
iex(30)> round 7.1
7

Thao tác với String

Chú ý: Để tương thích với Unicode, các function trong String hầu hết có độ phức tạp là O(n), khá chậm. Thậm chí nếu bạn cần lấy ký tự ở vị trí thứ N, Elixir cũng phải đi lần lượt từng ký tự cho đến ký tự thứ N. Nếu không cần xử lý string Unicode, có thể dùng các binary function để có tốc độ O(1). (Xem chi tiết trong tài liệu của String module).

iex(30)> String.length("abcdef")
6
iex(31)> byte_size("abcdef")
6
iex(32)> String.at("abcdef", 5)
"f"
iex(33)> :binary.at("abcdef", 5)
102
iex(34)> binary_part("abcdef", 5, 1)
"f"
iex(2)> Integer.to_string(42) <> List.to_string(["a", "b"])
"42ab"
iex(9)> String.contains?("Python", "on")
true
iex(10)> String.contains?("Python", "ON")
false

Chú ý tên function có dấu ?

iex(11)> String.starts_with?("Python", "Py")
true
iex(12)> String
String      StringIO
iex(12)> String.ends_with?("Python.mp3", ".mp3")
true

Dễ thấy, những function trả về true/false đều được đặt tên kết thúc bằng dấu ?

iex(55)> Enum.join(["Python", "PyMi.vn"], " ")
"Python PyMi.vn"
iex(57)> ["Elixir", "PyMi.vn"] |> Enum.join("+")
"Elixir+PyMi.vn"
iex(61)> "Học Python" <> " " <> "tại PyMi.vn"
"Học Python tại PyMi.vn"

iex(7)> Enum.join {1, 2}
** (Protocol.UndefinedError) protocol Enumerable not implemented for {1, 2}
    (elixir) lib/enum.ex:1: Enumerable.impl_for!/1
    (elixir) lib/enum.ex:116: Enumerable.reduce/3
    (elixir) lib/enum.ex:1636: Enum.reduce/3
    (elixir) lib/enum.ex:1154: Enum.join/2
iex(7)> Enum.join [1, 2]
"12"
iex(63)> String.split("Mình thích thì mình học \tthôi", " ")
["Mình", "thích", "thì", "mình", "học", "\tthôi"]
iex(64)> String.split("Mình thích thì mình     học \tthôi")
["Mình", "thích", "thì", "mình", "học", "thôi"]

Giống như Python.

Nhưng Elixir còn có thể Split tại nhiều "separator":

iex(7)> String.split("a-b+c", ["-", "+"])
["a", "b", "c"]
iex(14)> String.trim("   a\t abc\n")
"a\t abc"
iex(15)> String.trim_leading("   a\t abc\n")
"a\t abc\n"
iex(16)> String.trim_trailing("   a\t abc\n")
"   a\t abc"
iex(17)> String.replace("Python", "Py", "Jy")
"Jython"
iex(18)> String.upcase("Python")
"PYTHON"
iex(19)> String.downcase("PYThon")
"python"
iex(20)> String.capitalize("python is an animal")
"Python is an animal"
iex(22)> String.to_integer("   42  ")
** (ArgumentError) argument error
    :erlang.binary_to_integer("   42  ")
iex(22)> String.to_integer("42")
42

Function này trong Elixir không có khả năng bỏ đi whitespace như Python:

In [1]: int("   42 \n\t   ")
Out[1]: 42
iex(59)>  String.length("Lạc trôi")
8
iex(60)>  String.length("Lạc trôi😝")
9
iex(1)> String.slice("Python", 0, 2)
"Py"
iex(2)> String.slice("Python", 2, 10)
"thon"
iex(4)> String.reverse("DOICAN")
"NACIOD"

https://hexdocs.pm/elixir/1.4.2/String.html#content

List

Sử dụng module List. Các function có sẵn của module List không giống như trong Python, bởi List trong Python có thể thay đổi được (thêm bớt, sửa phần tử) còn List trong Elixir thì không.

iex(8)> li = [1, "PyMi", "Python", "Elixir"]
[1, "PyMi", "Python", "Elixir"]
iex(10)> i li
Term
  [1, "PyMi", "Python", "Elixir"]
Data type
  List
Reference modules
  List

Từ các kiểu dữ liệu khác:

String thành charlist:

iex(5)> String.to_charlist "Elixir"
'Elixir'

String thành list

iex(7)> String.graphemes("Việt Nam")
["V", "i", "ệ", "t", " ", "N", "a", "m"]
iex(11)> hd li
1
iex(12)> tl li
["PyMi", "Python", "Elixir"]
iex(24)> [head | tail] = ["Python", "PyMi", "Golang", "FAMILUG.org"]
["Python", "PyMi", "Golang", "FAMILUG.org"]
iex(25)> head
"Python"
iex(26)> tail
["PyMi", "Golang", "FAMILUG.org"]
iex(9)> List.first ["Python", "Golang", "Elixir"]
"Python"
iex(10)> List.last ["Python", "Golang", "Elixir"]
"Elixir"
iex(13)> List.wrap("Lac troi")
["Lac troi"]
iex(14)> List.wrap(0)
[0]
iex(15)> List.wrap(nil)
[]
iex(14)> length li
4
iex(5)> Enum.member?([1,2,4], 3)
false

Hay dùng cú pháp left in right:

iex(21)> 1 in [1, 2, 3]
true

Chú ý cú pháp in này không hoạt động với tuple, map (như trong Python).

iex(21)> 1 in {1, 2, 3}
** (Protocol.UndefinedError) protocol Enumerable not implemented for {1, 2, 3}
    (elixir) lib/enum.ex:1: Enumerable.impl_for!/1
    (elixir) lib/enum.ex:131: Enumerable.member?/2
    (elixir) lib/enum.ex:1352: Enum.member?/2
iex(22)> :a in %{:a => value}
** (CompileError) iex:22: undefined function value/0
    (elixir) expanding macro: Kernel.in/2
             iex:22: (file)
iex(18)> li ++ [2, "Erlang"]
[1, "PyMi", "Python", "Elixir", 2, "Erlang"]
iex(22)> [1, 2, 3, 2, 1] -- [2]
[1, 3, 2, 1]
iex(23)> [1, 2, 3, 2, 1] -- [2, 3]
[1, 2, 1]
iex(21)> List.insert_at(li, 0, "Zero")
["Zero", 1, "PyMi", "Python", "Elixir"]
iex(27)> l3 = ["Python", "PyMi", "Golang", "FAMILUG.org"]
["Python", "PyMi", "Golang", "FAMILUG.org"]
iex(28)> ["h" | l3]
["h", "Python", "PyMi", "Golang", "FAMILUG.org"]
iex(30)> l3 ++ ["t"]
["Python", "PyMi", "Golang", "FAMILUG.org", "t"]
iex(33)> List.replace_at(["Python", "PyMi", "Golang", "FAMILUG.org", "t"], 2, "Elixir")
["Python", "PyMi", "Elixir", "FAMILUG.org", "t"]
iex(31)> List.delete(["Python", "PyMi", "Golang", "FAMILUG.org", "t"], "Golang")
["Python", "PyMi", "FAMILUG.org", "t"]
iex(32)> List.delete_at(["Python", "PyMi", "Golang", "FAMILUG.org", "t"], 2)
["Python", "PyMi", "FAMILUG.org", "t"]
iex(11)> List.flatten ["Python", ["Erlang", "Elixir"]]
["Python", "Erlang", "Elixir"]
iex(36)>  Enum.slice(["Python", "PyMi", "Elixir", "FAMILUG.org"], 1, 10)
["PyMi", "Elixir", "FAMILUG.org"]

Chú ý argument thứ 3 là só phần tử sẽ slice, không phải index kết thúc như trong Python.

Theo bảng chữ cái

iex(37)> Enum.sort(["Python", "PyMi", "Elixir", "FAMILUG.org", "t"])
["Elixir", "FAMILUG.org", "PyMi", "Python", "t"]

Theo tiêu chí nhất định (function)

iex(35)> Enum.sort_by(["Python", "PyMi", "Elixir", "FAMILUG.org", "t"], &String.length/1)
["t", "PyMi", "Python", "Elixir", "FAMILUG.org"]

https://hexdocs.pm/elixir/Enum.html#content

Tuple

Luôn chú ý rằng trong Elixir, mọi kiểu dữ liệu đều là immutable, nên mọi function thay đổi dữ liệu đều trả về một giá trị mới, không thay đổi đầu vào đã gọi với function.

Thay một phần tử tại index

iex(2)> put_elem {:foo, "bar"}, 1, "bye"
{:foo, "bye"}

Thêm một giá trị vào index

iex(4)> Tuple.insert_at {"toi", "em"}, 1, "thang ay"
{"toi", "thang ay", "em"}

Thêm vào cuối tuple:

iex(5)> Tuple.append {6, 9}, "Tail"
{6, 9, "Tail"}

Xóa tại index

iex(6)> Tuple.delete_at {"Elixir", "is", :not, "great"}, 2
{"Elixir", "is", "great"}

Biến thành list

iex(7)> Tuple.to_list {:x, :y, :z}
[:x, :y, :z]

Map

là một cấu trúc dữ liệu có thể gọi là "key-value" store. Tên khác (trong ngôn ngữ khác): dictionary, associative array

map = %{:a => 1, :b => 2}
iex(47)>  %{:a => 1, :b =>2}
%{a: 1, b: 2}
iex(48)> %{"name" => "PYMI", "country" => "VN"}
%{"country" => "VN", "name" => "PYMI"}
iex(59)> [{1, 7}, {2, 3}] |> Map.new
%{1 => 7, 2 => 3}
iex(60)> [{1, 7}, {2, 3}] |> Map.new |> Map.to_list
[{1, 7}, {2, 3}]
iex(68)> %{:name => who} = %{:name => "HVN"}
%{name: "HVN"}
iex(69)> who
"HVN"

iex(44)> map = %{:a => 1, :b =>2}
%{a: 1, b: 2}
iex(46)> map[:b]
2
iex(47)> map[:c]
nil

Hay

iex(60)> Map.get(map, :b)
2
iex(64)> Map.get(%{first_name: "Jon", last_name: "Snow"}, :age)
nil

Hoặc

iex(2)> Map.fetch(%{first_name: "Jon", last_name: "Snow"}, :last_name)
{:ok, "Snow"}
iex(3)> Map.fetch(%{first_name: "Jon", last_name: "Snow"}, :age)
:error
iex(3)> Map.has_key?(%{:name => "HVN"}, :age)
false
iex(62)> Map.put(%{:a => 1}, :h, 4)
%{a: 1, h: 4}
iex(1)> Map.delete(%{:name => "HVN", :age => 27}, :age)
%{name: "HVN"}

Xoá key không tồn tại không ảnh hưởng gì:

iex(2)> Map.delete(%{:name => "HVN", :age => 27}, :language)
%{age: 27, name: "HVN"}
iex(6)> Map.merge(%{first_name: "Jon", last_name: "Snow"}, %{last_name: "Water", age: 27})
%{age: 27, first_name: "Jon", last_name: "Water"}
iex(63)> Map.to_list(%{:name => "HVN", :age => 27})
[age: 27, name: "HVN"]
iex(65)> %{ map | :b => 5}
%{a: 1, b: 5}
iex(66)> %{ map | :c => 5}
** (KeyError) key :c not found in: %{a: 1, b: 2}
iex(66)> student = %{:name => "HVN"}
%{name: "HVN"}
iex(67)> student.name
"HVN"
iex(70)> users = [hvn: %{name: "HVN", age: 27, language: ["Erlang", "Elixir", "Python"]},
hails: %{name: "HaiLS", age: 26, language: ["Golang", "JS"]}]
[hvn: %{age: 27, language: ["Erlang", "Elixir", "Python"], name: "HVN"},
 hails: %{age: 26, language: ["Golang", "JS"], name: "HaiLS"}]

iex(72)> put_in users[:hvn].age, 29
[hvn: %{age: 29, language: ["Erlang", "Elixir", "Python"], name: "HVN"},
 hails: %{age: 26, language: ["Golang", "JS"], name: "HaiLS"}]
iex(2)>  %{a: [1, 2, 3], b: ["meo", "ga"]} |> Enum.into(%{}, fn {k, v} -> {k, length(v)} end)
%{a: 3, b: 2}

Mỗi kiểu dữ liệu đều có một module tương ứng cung cấp các function cần thiết: List, Map, String. Module Enum dùng chung cho các kiểu dữ liệu cho phép duyệt qua từng phần tử: như List

MapSet

iex(12)> MapSet.member?(MapSet.new([1, 3, 43, 98]) , 9)
false
iex(16)> MapSet.intersection(MapSet.new([1,3,5]), MapSet.new([1,5,2]))
#MapSet<[1, 5]>
#MapSet<[1, 2, 3, 5]>

Struct

Lấy giá trị của field

iex(1)> defmodule Person do
...(1)>   defstruct [:name, :age]
...(1)> end
...
iex(3)> Map.fetch %Person{name: "HVN", age: 27}, :name
{:ok, "HVN"}

Thay đổi giá trị của các field trong struct

iex(4)> hvn = %Person{name: "HVN", age: 27}
%Person{age: 27, name: "HVN"}
iex(5)> %{hvn | age: hvn.age + 1}
%Person{age: 28, name: "HVN"}

Thay đổi giá trị của nested value (value của field trong struct trong struct khác ...)

iex(2)> defmodule Class do
...(2)>   defstruct person: %Person{}, name: "PyMI"
...(2)> end
...
iex(4)> pymi = %Class{person: %Person{name: "HVN", age: 27}}
%Class{name: "PyMI", person: %Person{age: 27, name: "HVN"}}
iex(5)> put_in pymi.person.age, 28
%Class{name: "PyMI", person: %Person{age: 28, name: "HVN"}}

Pattern matching

= : the match operator - Khi gán biến, biến phải nằm bên trái dấu =

iex(7)> x = 5
5
iex(8)> 6 = y
** (CompileError) iex:8: undefined function y/0
iex(18)> {name, age} = {"PyMI.vn", 2}
{"PyMI.vn", 2}
iex(19)> name
"PyMI.vn"
iex(20)> age
2

iex(1)> [x, y, z] = [1, 2, 4]  # list
[1, 2, 4]
iex(2)> x
1
iex(3)> z
4

Khi bên phải chứa giá trị thay vì biến, 2 bên chỉ match nhau nếu giá trị bên tay trái match giá trị bên tay phải:

iex(4)> {:ok, 1, salary} = {:ok, 1, 15}
{:ok, 1, 15}
iex(5)> {:ok, 1, income} = {:ok, 3, 20}
** (MatchError) no match of right hand side value: {:ok, 3, 20}

iex(10)> {x, x} = {1, 1}
{1, 1}
iex(11)> {x, x} = {1, 2}
** (MatchError) no match of right hand side value: {1, 2}


Khi 2 bên không "match", MatchError sẽ xảy ra.

iex(5)> [h | t] = [1, 2, 3, 4]
[1, 2, 3, 4]
iex(6)> h
1
iex(7)> t
[2, 3, 4]

Giống như gọi function hdtl:

iex(8)> hd [1, 2, 3, 4]
1
iex(9)> tl [1, 2, 3, 4]
[2, 3, 4]

Cú pháp này rõ ràng không match list rỗng:

iex(10)> [h | t] = []
** (MatchError) no match of right hand side value: []

iex(11)> x = 5
5
iex(12)> x = 7
7
iex(13)> x
7
iex(14)> ^x = 8
** (MatchError) no match of right hand side value: 8
Vì giống như viết: 7 = 8
iex(14)> 7 = 8
** (MatchError) no match of right hand side value: 8

Control flow: case, cond, if, unless, do/end

if

if/2 là một function, tức nó sẽ trả về một giá trị sau khi chạy. Khác với Python hay nhiều ngôn ngữ khác, if trong Python là một "statement" (câu lệnh), và nó chỉ thực hiện điều khiển luồng chứ không trả về giá trị nào. (PS: thực sự if/2 mà 1 macro - khái niệm macro sẽ được nói sau)

iex(15)> k = if 5 > 4 do
...(15)> "yeah"
...(15)> end
"yeah"
iex(16)> k
"yeah"

if/2 trả về nil nếu điều kiện nó nhận được trả về false hay nil.

iex(17)> h = if 5 < 4 do
...(17)> "hey"
...(17)> end
nil
iex(18)> h
nil

Mẫu cú pháp:

if condition do
SOMETHING
end

if có thể đi kèm với else:

iex(19)> m = if 5 < 4 do
...(19)> "smaller"
...(19)> else
...(19)> "bigger"
...(19)> end
"bigger"
iex(20)> m
"bigger"

Trong Elixir chỉ có if/else/end không có elif như trong Python, khi cần xử lý nhiều trường hợp, ta có thể dùng cond.

Đọc thêm: https://hexdocs.pm/elixir/Kernel.html#if/2

cond

cond do ... end cho phép xử lý nhiều điều kiện, giống như elif hay else if trong các ngôn ngữ khác:

iex(24)> x = 18
18
iex(25)> cond do
...(25)>   x < 18 ->
...(25)>     "Not permitted"
...(25)>   x == 18 ->
...(25)>     "Okay"
...(25)>   x > 18 ->
...(25)>     "Too old"
...(25)> end
"Okay"

unless

Như if, nhưng ngược lại.

iex(26)> x = 18
18
iex(27)> unless x < 18 do
...(27)>   "got this"
...(27)> end
"got this"
iex(28)> if x < 18 do
...(28)>   "no"
...(28)> end
nil

case

case sử dụng pattern matching, và chỉ dừng lại khi ta tìm thấy giá trị nào match. Ta thấy case chỉ dùng để kiểm tra xem 1 giá trị có bằng một giá trị khác, chứ không dùng so sánh >, < ... như if.

iex(22)> case 5 do
...(22)>   4 ->
...(22)>
...(22)> "Won't match"
...(22)> _ ->
...(22)> "will match"
...(22)> end
"will match"
iex(23)> case {1, 2, 3} do
...(23)>   {1, x, 3} when x > 0 ->
...(23)>     "will match"
...(23)>   _ ->
...(23)>     "Would match, if guard cond were not sastified"
...(23)> end
"will match"

when x > 0 gọi là guard condition. Ta thấy case cũng là một macro chứ không phải "statement" như trong các ngôn ngữ khác.

Cú pháp:

case SOMETHING do
  CLAUSE1 [GUARD] ->
      "OTHERTHING"
  CLAUSE2 [GUARD] ->
     ...
  _ ->
     "LAST PATTERN MATCH REMAIN"
end

Nếu không clause nào match, error sẽ được raise:

iex(24)> case :ok do
...(24)>   :error ->
...(24)>     "Not match"
...(24)> end
** (CaseClauseError) no case clause matching: :ok

Guard

Trong guard không được dùng && || ! Error trong guard sẽ khiến guard fail.

do/end

Trong if unless cond case đều có dùng do và kết thúc bằng end.

if true do
...
end

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

if true, do: (
...
)

do/end là syntactix sugar (cú pháp để viết cho dễ).

Function

Gọi function (calling):

Cú pháp thông thường:

iex(1)> String.to_integer(String.trim("  42 \n "))
42
iex(7)> String.split("42-15\n17", ["-", "\n"])
["42", "15", "17"]

Cú pháp bỏ dấu ():

iex(8)> String.split "42-15\n17", ["-", "\n"]
["42", "15", "17"]

iex(10)> Enum.map(String.split("42-15\n17", ["-", "\n"]), fn(x) -> String.to_integer(x) end)
[42, 15, 17]

Toán tử pipe (pipe operator)

Cú pháp sử dụng "pipe", dữ liệu sẽ chạy từ output của 1 function, qua pipe và trở thành argument đầu tiên của function tiếp theo (giống Pipeline trên UNIX shell).

iex(11)> String.split("42-15\n17", ["-", "\n"]) |> Enum.map(fn(x) -> String.to_integer(x) end)
[42, 15, 17]

iex(23)> 1..1000 |> Enum.filter(fn(x) -> (rem(x, 3) == 0 || rem(x, 5) == 0) end) |> Enum.sum
234168

Định nghĩa function

Function có tên phải được định nghĩa trong module. Function sẽ trả về giá trị cuối cùng nó tính được - không có câu lệnh "return". Đoạn code sau định nghĩa module Math và function sum trả về tổng của 2 argument a, b:

defmodule Math do
  def sum(a, b) do
    a + b
  end
end

hoặc

defmodule Hacker, do: def hack(x), do: x*2

khi chỉ có 1 function và function chỉ có 1 dòng.

defmoduledef đều là các macro.

Private function

Định nghĩa bằng macro defp/2, từ module khác không thể chạy các function này.

Ký hiệu (notation)

Khi nhắc tới function, trong Elixir dùng ký hiệu: name/arity với name là tên , và arity là số argument function đó nhận.

Math.sum ở trên ký hiệu là Math.sum/2

Multiple clause function

Một function có thể có nhiều clause:

Mỗi "clause" thường đi kèm một "guard", nếu argument pass vào match với argument của clause và guard đi kèm trả về true, clause đó sẽ được gọi.

defmodule Foo do
  def rprint(msg, n) when n <= 1 do
    IO.puts msg
  end

  def rprint(msg, n) do
    IO.puts msg
    rprint(msg, n - 1)
  end
end

Foo.rprint("Hello", 5)

Function capturing

TODO

Default argument

def join(a, b, sep \\ ",") do
  a <> sep <> b
end

"," là default argument, khi gọi funtion mà không pass giá trị cho argument sep, sep sẽ sử dụng giá trị mặc định ",".

Các phần của một function

def sum(a, b) do
  a + b
end

Khi function với default value có nhiều mệnh đề, phải khai báo một function head để khai báo giá trị default.

TODO more detail

Module

Compile

Có thể viết module vào file NAME.ex: math.ex.

Compile module:

$ elixirc math.ex  # tạo ra file Elixir.Math.beam

Bật iex cùng thư mục sẽ tự động load module, chỉ việc gọi.

Script

Lưu file với đuôi .exs, Elixir sẽ hiểu đó là 1 "script" và sẽ không tạo file có đuôi .beam nữa, chạy nó như chạy các script khác (Python, bash...):

$ elixir math.exs

Loop bằng recursion, reduce và map

Trong Elixir, mọi thứ đều là "immutable" (không thay đổi được), vì vậy những khái niệm để lặp như trong các ngôn ngữ C, Python, Java, PHP, Golang ... sẽ không được ứng dụng:

for i in 'Elixir':
    print(i)

Ở vòng lặp for này, giá trị của i lần lượt thay đổi thành các ký tự trong "Elixir" -> không đảm bảo được tính immutable của Elixir.

i = 0
while True:
    print(i)
    i = i + 1

Tương tự, trong vòng lặp while này i cũng thay đổi sau mỗi vòng lặp, không đảm bảo tính immutable.

Elixir hay các ngôn ngữ lập trình hàm (functional programming language) khác sử dụng recursive function để tạo hiệu ứng/ kết quả như loop.

Recursive function

Là function mà bên trong phần body, nó tự gọi đến chính nó cho đến khi gặp một điều kiện để dừng lại. Sẽ không có gì bị thay đổi khi dùng recursive function, bởi ta sẽ sinh ra giá trị mới, chứ không thay đổi giá trị cũ. Mọi khái niệm sẽ rõ ràng khi thử với function tính giai thừa của 1 số:

Giai thừa của một số nguyên không âm được tính bằng tích của số đó nhân với giai thửa của số nhỏ hơn nó 1 đơn vị. Hay viết ở dạng công thức toán: factorial(n) = n * factorial(n - 1) khi n = 0 thì factorial(0) = 1. # điều kiện dừng

Thử tính factorial của 3:

factorial(3) = 3 * factorial(2) factorial(2) = 2 * factorial(1) factorial(1) = 1 * factorial(0) factorial(0) = 1

Sau khi đã chạm đến điều kiện dựng, ta lấy kết quả thu được thay ngược lên trên. factorial(1) = 1 * factorial(0) = 1 * 1 = 1 factorial(2) = 2 * factorial(1) = 2 * 1 = 2 factorial(3) = 3 * factorial(2) = 3 * 2 = 6

Kết quả là factorial(3) bằng 6.

defmodule Rescusion do
  def fact(n) when n <= 0 do
    1
  end

  def fact(n) do
    n * fact(n - 1)
  end
end

Trong bài này, code của Elixir chỉ đơn giản là chuyển công thức toán học thành code.

Map

Một việc làm thường xuyên khi sử dụng loop là để biến 1 tập giá trị, thành 1 tập giá trị khác.

Ví dụ, cho một list L = [1, 2, 3 ,4], cần thu được kết quả là một list mà phần tử của nó là mỗi phần tử của L nhân với 2.

Việc biến một tập thành một tập khác bằng cách gọi function với mỗi phần tử của tập gọi là "mapping" (ánh xạ trong toán học). Ta "map" một phần tử từ tập ban đầu thành phần tử trong tập mới.

Viết function để map list nói trên:

iex(41)> defmodule Double do
...(41)>   def double_each([head | tail]) do
...(41)>     [head * 2 | double_each(tail)]
...(41)>   end
...(41)>
...(41)>   def double_each([]) do
...(41)>     []
...(41)>   end
...(41)> end
{:module, Double,
...
iex(42)> Double.double_each([1,2,3,4])
[2, 4, 6, 8]

Có thể dùng function có sẵn Enum.map/2 để thực hiện mapping:

iex(45)> Enum.map([1,2,3,4], fn(x) -> x * 2 end)
[2, 4, 6, 8]

Reduce

Một ứng dụng khác thường dùng khi lặp là để tính một giá trị nào đó sẽ thu được sau khi duyệt qua tất cả giá trị trong tập, như tính tổng, tích của tập.

Ở đây ta biến từ 1 tập nhiều phần tử thành 1 giá trị cuối cùng. Việc "thu gọn" này có tên là "reducing".

Map và reduce là 2 thuật toán cốt lõi của "big data".

iex(46)> defmodule Reduce do
...(46)>   def sum_list([head | tail], accumulator) do
...(46)>     sum_list(tail, head + accumulator)
...(46)>   end
...(46)>
...(46)>   def sum_list([], accumulator) do
...(46)>     accumulator
...(46)>   end
...(46)> end

iex(47)> Reduce.sum_list([1,2,3], 0)
6

Hay dùng module có sẵn Enum.reduce/2:

iex(48)> Enum.reduce([1,2,3], 0, fn(x, acc) -> x + acc end)
6

IO - xử lý dữ liệu vào ra.

In ra màn hình

Mặc dù hầu hết các sách dạy lập trình / trường học sẽ luôn bắt đầu bằng việc dạy "print" ra màn hình (và đi kèm là đọc những gì người dùng nhập vào), nhưng trên thực tế, có khoảng < 5% người thực sự dùng print trong chương trình của mình.

Hãy thử nghĩ với người dùng Windows, có bao giờ bạn bật cmd lên và gõ lệnh? Các chương trình đều giao tiếp với người dùng qua giao diện đồ hoạ / web, chứ không phải các dòng lệnh. Những người làm việc với dòng lệnh chủ yếu là các Linux Sysadmin / lập trình viên / hacker. Nếu bạn là một web developer, bạn đưa nội dung ra trang web chứ không print nó.

Elixir sử dụng module IO cho các thao tác này:

iex(49)> IO.puts "Hello"
Hello
:ok
iex(50)> IO.puts "Hello FAMILUG"
Hello FAMILUG
:ok
iex(51)> answer = IO.gets("yes or no? ")
yes or no? yes
"yes\n"
iex(52)> IO.puts(answer)
yes

:ok

Có thể ghi ra stderr:

iex(53)> IO.puts(:stderr, "Hello standard error")
Hello standard error
:ok

Để không thêm ký tự newline (\n) sau mỗi dòng, sử dụng IO.write thay vì IO.puts.

puts chỉ nhận argument là string, muốn "in ra" một list, tuple hay object bất kỳ, hãy dùng IO.inspect.

Khi dùng IO.inspect in ra list có nhiều phần tử (>50), Elixir sẽ chỉ in ra 50 phần tử đầu và ghi ... để ký hiệu còn tiếp. Muốn hiện đầy đủ, có thể gọi thêm argument:

iex(9)> IO.inspect(1..100|>Enum.map(fn x -> x*2 end))

[2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38,
 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74,
 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, ...]

iex(10)> IO.inspect(1..100|>Enum.map(fn x -> x*2 end), limit: :infinity)
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42,
 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82,
 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116,
 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148,
 150, 152, 154, 156, 158, 160, 162, 164, 166, 168, 170, 172, 174, 176, 178, 180,
 182, 184, 186, 188, 190, 192, 194, 196, 198, 200]

Đọc ghi file

Module File chứa các function để tương tác với file, từ đọc, ghi, xoá, copy...

iex(53)> IO.puts(:stderr, "Hello standard error")
Hello standard error
:ok
iex(54)> {:ok, file} = File.open("hellofile.txt", [:write])
{:ok, #PID<0.260.0>}
iex(55)> IO.binwrite(file, "Hello world!")
:ok
iex(56)> File.close(file)
:ok
iex(57)> File.read("hellofile.txt")
{:ok, "Hello world!"}

Các function xử lý file:

File.rm/1, File.mkdir/1, File.cp_r/2, ...

Xử lý lỗi

Errror (hay exception)

iex(58)> "abc" + 1
** (ArithmeticError) bad argument in arithmetic expression
    :erlang.+("abc", 1)
iex(58)> raise "oizoioi"
** (RuntimeError) oizoioi

iex(58)> raise ArgumentError, message: "invalid argument"
** (ArgumentError) invalid argument

try/rescue/catch/after

Tương tự như try/except trong Python hay try/catch trong Java, Elixir có try/recuse và try/catch, nhưng trong Elixir, sử dụng chúng là điều không nên / hiếm khi dùng.

iex(61)> try do
...(61)>   r = 1/0
...(61)> rescue
...(61)>   ArithmeticError -> "Error"
...(61)> end
"Error"

Vậy Elixir làm gì khi gặp lỗi? có "exception" thì xử lý thế nào? Trong triết lý của Erlang/Elixir, "lỗi" là một phần của chương trình, và hệ thống nằm dưới ngôn ngữ (BEAM/OTP) sẽ xử lý chúng một cách ngon lành. Tạm thời bỏ qua ở đây.

Bắt đầu code một project

Đến đây đã đủ các công cụ cơ bản để viết những chương trình bình thường / luyện tập với các thuật toán ... cho đến khi quen với ngôn ngữ. Elixir còn nhiều khái niệm khác, đặc biệt được đưa ra để xử lý trong môi trường "phân tán", nhưng tập trung vào những tính năng đó ngay bây giờ chỉ làm cho người học bị quá tải với những khái niệm mới lạ, trong khi vẫn chưa nắm rõ phần cơ bản. Vậy nên, các khái niệm "khác" đó sẽ được dành cho phần sau. Còn bây giờ, tạo một project Elixir và code:

Mix là "build tool" của Elixir, để tạo một project mới, dùng câu lệnh:

 mix new hello_familug
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/hello_familug.ex
* creating test
* creating test/test_helper.exs
* creating test/hello_familug_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd hello_familug
    mix test

Run "mix help" for more commands.

Sửa nội dung file lib/hello_familug.ex như sau:

defmodule HelloFamilug do
  def main(args) do
    IO.puts "Hello FAMILUG!"
  end
end

Thêm dòng:

      escript: escript,

vào sau dòng

      elixir: "~> VERSION",

trong mix.exs.

Thêm function sau vào trong file mix.exs

  def escript do
    [main_module: HelloFamilug]  # tên module sẽ được chạy
  end

Compile và chạy:

$ mix escript.build
Compiling 1 file (.ex)
warning: variable args is unused
  lib/hello_familug.ex:2

Generated hello_familug app
Generated escript hello_familug with MIX_ENV=dev
$ ./hello_familug
Hello FAMILUG!

Sửa lại code để nhận vào input từ người dùng:

defmodule HelloFamilug do
  def main(args) do
    {_, [name], _} = OptionParser.parse(args)
    IO.puts "Hello " <> name
  end
end

Tạm thời bỏ qua chi tiết OptionParser.parse/1 làm gì, build lại và gọi với một cái tên:

$ mix escript.build
Compiling 1 file (.ex)
Generated escript hello_familug with MIX_ENV=dev
$ ./hello_familug Python
Hello Python

Chương trình dòng lệnh (CLI tool) không phải là thế mạnh của Elixir, nhưng nó hoàn toàn có thể làm được và không hề phức tạp. Với từng ấy đủ để ta bắt đầu cuộc hành trình vào những giấc mơ sâu vô tận trong thế giới của nhà giả kim và Elixir (thuốc tiên).

FAQs

Tại sao tên lại nhảm nhí vậy "học Elixir trong một giấc mơ"?

Vì việc đọc tiếng Anh ở Việt Nam rất "thảm hoạ". Chữ "Python" - rõ ràng đọc là "pai-thon" thì phần lớn lập trình viên lại đọc là "Pi-thông". Vậy nên tôi đặt tên có vần để người học có thể đọc đúng "Học E lík xơ trong một giấc mơ".