cv.cm

Lập trình · Ngôn ngữ lập trình

Hướng dẫn học lập trình Elixir cơ bản cho người mới

8 phút đọc

Trả lời nhanh

Elixir là ngôn ngữ hàm, kiểu động, chạy trên máy ảo Erlang (BEAM), được thiết kế cho các hệ thống mở rộng được, chịu lỗi và đồng thời. Các ý tưởng cốt lõi là dữ liệu bất biến, khớp mẫu, toán tử pipe và các process nhẹ giao tiếp bằng tin nhắn. Trang này bao gồm cài đặt, kiểu dữ liệu, khớp mẫu, điều khiển luồng, module và hàm, collection, process và xử lý lỗi.

Mở Kiểm tra regex

Elixir là gì và dùng để làm gì

Elixir là ngôn ngữ hàm, kiểu động, do José Valim tạo ra và phát hành lần đầu năm 2012. Nó chạy trên BEAM, máy ảo của Erlang, nên thừa hưởng process nhẹ, khả năng chịu lỗi và hỗ trợ hệ thống phân tán của Erlang, đồng thời bổ sung cú pháp thân thiện, macro và công cụ hiện đại.

Elixir được dùng cho ứng dụng web và API với framework Phoenix, các tính năng thời gian thực như chat và dashboard trực tiếp (Phoenix LiveView), hệ thống nhắn tin, đường ống dữ liệu, thiết bị nhúng với Nerves và học máy với Nx. Các công ty chọn nó cho những dịch vụ phải xử lý nhiều kết nối đồng thời và tiếp tục chạy khi một phần bị lỗi.

Cài Elixir và chạy Hello, world

Cài Elixir từ elixir-lang.org (nó cũng cài hoặc yêu cầu Erlang/OTP). Script dùng đuôi .exs; tệp dự án được biên dịch dùng .ex. iex khởi động shell tương tác.

# hello.exs
IO.puts("Hello, world!")
elixir hello.exs
# Hello, world!
iex                      # interactive shell; type h(Enum.map) for help
mix new my_app           # create a new project with Mix, the build tool

Chú thích bắt đầu bằng #. Dấu ngoặc quanh đối số hàm là tùy chọn ở nhiều chỗ, nên IO.puts "hi" cũng chạy được.

Các kiểu cơ bản và tính bất biến

Mọi dữ liệu trong Elixir đều bất biến. Gán lại một tên bằng = làm nó trỏ tới giá trị mới; giá trị cũ không bao giờ bị thay đổi. Chuỗi là binary UTF-8 viết trong dấu nháy kép, còn atom là hằng số có tên bắt đầu bằng dấu hai chấm.

name = "Elixir"
year = 2012
IO.puts("#{name} since #{year}")    # Elixir since 2012
IO.puts(10 / 4)                     # 2.5 (always a float)
IO.puts(div(10, 4))                 # 2
IO.puts(rem(10, 4))                 # 2
IO.puts("ab" <> "cd")               # abcd (string concatenation)
IO.puts(String.length("héllo"))     # 5 (counts characters, not bytes)
IO.inspect(:ok)                     # :ok
IO.inspect(is_atom(true))           # true (booleans are atoms)
KiểuVí dụGhi chú
integer42, 1_000_000độ chính xác tùy ý
float3.14độ chính xác kép 64 bit
atom:ok, :errorhằng số; true, false và nil là atom
string"hello"binary UTF-8; nội suy bằng #{}
list[1, 2, 3]danh sách liên kết
tuple{:ok, 1}kích thước cố định, lưu liên tục trong bộ nhớ
map%{"a" => 1}kho khóa-giá trị

Khớp mẫu

Trong Elixir, = là toán tử khớp. Vế trái là một mẫu; nếu vế phải khớp, các biến trong mẫu sẽ được gán. Khớp mẫu được dùng ở khắp nơi: trong phép gán, case, đầu hàm và xử lý lỗi.

{:ok, value} = {:ok, 42}
IO.puts(value)                         # 42

[first | rest] = [1, 2, 3]
IO.inspect({first, rest})              # {1, [2, 3]}

%{name: n} = %{name: "Ann", age: 31}
IO.puts(n)                             # Ann

result = {:error, :not_found}
message =
  case result do
    {:ok, v} -> "got #{v}"
    {:error, reason} -> "failed: #{reason}"
  end
IO.puts(message)                       # failed: not_found

x = 1
^x = 1                                 # pin operator: match the current value

Nếu mẫu không khớp, Elixir phát sinh MatchError. Toán tử ghim ^ nghĩa là "dùng giá trị hiện có của biến này" thay vì gán lại.

Điều khiển luồng

Elixir có case, cond và if/unless, và tất cả đều trả về giá trị. Không có vòng lặp for/while thay đổi biến đếm; bạn dùng đệ quy, module Enum hoặc biểu thức for comprehension.

temp = 22
feel =
  cond do
    temp > 30 -> "hot"
    temp >= 15 -> "mild"
    true -> "cold"
  end
IO.puts(feel)                                   # mild

IO.puts(if(rem(temp, 2) == 0, do: "even", else: "odd"))  # even

squares = for n <- 1..5, rem(n, 2) == 1, do: n * n
IO.inspect(squares)                             # [1, 9, 25]

Chỉ false và nil là sai; mọi thứ khác, kể cả 0 và "", đều là đúng.

Hàm, module và toán tử pipe

Hàm có tên nằm trong module định nghĩa bằng defmodule. Một hàm có thể có nhiều mệnh đề, được chọn bằng khớp mẫu và guard (when). Hàm ẩn danh viết là fn ... end và được gọi bằng dấu chấm. Toán tử pipe |> truyền kết quả của một biểu thức làm đối số đầu tiên của lời gọi tiếp theo.

defmodule Math do
  def factorial(0), do: 1
  def factorial(n) when n > 0, do: n * factorial(n - 1)

  def sign(n) when n < 0, do: :negative
  def sign(0), do: :zero
  def sign(_n), do: :positive
end

IO.puts(Math.factorial(5))          # 120
IO.inspect(Math.sign(-3))           # :negative

double = fn x -> x * 2 end
IO.puts(double.(21))                # 42 (note the dot)

"  Elixir is fun  "
|> String.trim()
|> String.upcase()
|> String.split()
|> IO.inspect()                     # ["ELIXIR", "IS", "FUN"]

Hàm được xác định bằng tên và arity (số đối số), viết là String.split/1 hoặc Math.factorial/1 trong tài liệu.

Collection: danh sách, map, keyword list và Enum

Module Enum làm việc với mọi collection có thể duyệt và cung cấp map, filter, reduce, sort_by, group_by cùng nhiều hàm khác. Map là cấu trúc khóa-giá trị chính; keyword list, như [size: 10, color: :red], thường dùng cho các tùy chọn.

nums = [5, 3, 8, 1]
IO.inspect(Enum.sort(nums))                      # [1, 3, 5, 8]
IO.inspect(Enum.map(nums, &(&1 * 10)))           # [50, 30, 80, 10]
IO.inspect(Enum.filter(nums, fn n -> n > 2 end)) # [5, 3, 8]
IO.puts(Enum.reduce(nums, 0, &+/2))              # 17

user = %{name: "Ann", age: 31}
older = %{user | age: 32}                        # update an existing key
IO.puts(user.age)                                # 31 (unchanged)
IO.puts(older.age)                               # 32
with_city = Map.put(user, :city, "Oslo")
IO.puts(with_city.city)                          # Oslo
IO.inspect(Map.get(user, :email, "none"))        # "none"

Toán tử capture & tạo hàm ẩn danh ngắn: &(&1 * 10) nghĩa là fn x -> x * 10 end, và &+/2 chỉ hàm + với hai đối số.

Process và đồng thời

Tính năng đặc trưng của Elixir là process BEAM: một đơn vị thực thi rất nhẹ, tách biệt, có bộ nhớ riêng. Các process chỉ giao tiếp bằng cách gửi tin nhắn, và một máy có thể chạy hàng trăm nghìn process. Supervisor khởi động lại các process bị sập, đó là nền tảng cho cách tiếp cận chịu lỗi "let it crash" của Elixir.

parent = self()

spawn(fn ->
  send(parent, {:result, 6 * 7})
end)

receive do
  {:result, value} -> IO.puts("Got #{value}")   # Got 42
after
  1_000 -> IO.puts("timeout")
end

tasks = for n <- 1..3, do: Task.async(fn -> n * n end)
IO.inspect(Task.await_many(tasks))               # [1, 4, 9]

Trong ứng dụng thực tế, bạn hiếm khi gọi spawn trực tiếp. Bạn dùng các abstraction của OTP như Task, GenServer, Agent và Supervisor.

Xử lý lỗi

Elixir thành ngữ trả về tuple gắn thẻ như {:ok, value} hoặc {:error, reason} cho các lỗi có thể đoán trước, và dùng case hoặc with để xử lý. Các hàm có tên kết thúc bằng !, như File.read!/1, thì phát sinh ngoại lệ. Ngoại lệ có thể bắt bằng try/rescue, nhưng cách này không phổ biến.

case File.read("missing.txt") do
  {:ok, text} -> IO.puts(text)
  {:error, reason} -> IO.puts("Cannot read: #{reason}")   # Cannot read: enoent
end

with {a, ""} <- Integer.parse("40"),
     {b, ""} <- Integer.parse("2") do
  IO.puts(a + b)                                         # 42
else
  _ -> IO.puts("not a whole number")
end

try do
  String.to_integer("abc")
rescue
  e in ArgumentError -> IO.puts("Error: #{Exception.message(e)}")
end

with nối nhiều phép khớp và dừng ở phép đầu tiên không khớp, trả về giá trị đó. Nó tránh các biểu thức case lồng sâu.

Một chương trình hoàn chỉnh nhỏ: tần suất từ

Script này đếm từ trong một văn bản và in ba từ phổ biến nhất. Đây là một pipeline Elixir điển hình xây từ các hàm String và Enum.

defmodule WordFreq do
  def top(text, n) do
    text
    |> String.downcase()
    |> String.split(~r/[^a-z']+/, trim: true)
    |> Enum.frequencies()
    |> Enum.sort_by(fn {word, count} -> {-count, word} end)
    |> Enum.take(n)
  end
end

"The cat and the dog. The dog sat, and the cat ran."
|> WordFreq.top(3)
|> Enum.each(fn {word, count} -> IO.puts("#{word}: #{count}") end)
# the: 4
# and: 2
# cat: 2

Học tiếp ở đâu

  • Hướng dẫn Getting Started chính thức trên hexdocs.pm và website elixir-lang.org.
  • HexDocs, nơi công bố tài liệu của Elixir và mọi gói Hex.
  • Các hướng dẫn framework Phoenix cho phát triển web.
  • Elixir School, giáo trình cộng đồng miễn phí, và track Elixir của Exercism để luyện tập.

Câu hỏi thường gặp

Elixir và Erlang khác nhau thế nào?

Cả hai chạy trên cùng máy ảo BEAM và có thể gọi mã của nhau. Elixir bổ sung cú pháp lấy cảm hứng từ Ruby, macro, toán tử pipe và các công cụ như Mix và Hex, còn Erlang là ngôn ngữ cũ hơn mà OTP được xây dựng bằng nó.

Elixir có tốt cho phát triển web không?

Có. Framework Phoenix nhanh và xử lý được nhiều kết nối đồng thời, còn LiveView cho phép bạn xây trang tương tác bằng HTML render phía máy chủ với rất ít JavaScript.

Elixir có kiểu tĩnh không?

Elixir có kiểu động. Các phiên bản gần đây đang bổ sung vào trình biên dịch một hệ thống kiểu dần dựa trên lý thuyết tập hợp, và nhiều nhóm cũng dùng Dialyzer với typespec để tìm lỗi kiểu.

|> nghĩa là gì trong Elixir?

|> là toán tử pipe. x |> f(y) giống với f(x, y), nên một chuỗi biến đổi có thể đọc từ trên xuống dưới.

Tệp .ex và .exs là gì?

Tệp .ex được biên dịch như một phần của dự án, còn tệp .exs là script được thông dịch khi chạy, thường dùng cho kiểm thử và cấu hình.

Tất cả hướng dẫn lập trình