cv.cm

編碼 · 程式語言

Elixir 入門教學

閱讀約 9 分鐘

簡答

Elixir 是一種動態、函數式的語言,運行在 Erlang 虛擬機(BEAM)上,專為可擴充、容錯且高併發的系統而設計。它的核心概念是不可變資料、模式比對、管線運算子,以及透過訊息溝通的輕量行程。本頁涵蓋環境設定、型別、模式比對、流程控制、模組與函式、集合、行程與錯誤處理。

開啟正規測試

Elixir 是什麼、可以用來做什麼

Elixir 是由 José Valim 創造的動態函數式語言,2012 年首次發布。它運行在 Erlang 的虛擬機 BEAM 上,因此繼承了 Erlang 的輕量行程、容錯能力與分散式系統支援,同時加入了親切的語法、巨集與現代化的工具鏈。

Elixir 用於以 Phoenix 框架開發的網頁應用與 API、聊天與即時儀表板等即時功能(Phoenix LiveView)、訊息系統、資料管線、搭配 Nerves 的嵌入式裝置,以及搭配 Nx 的機器學習。需要處理大量並行連線、且部分元件故障時仍要持續運作的服務,常會選擇它。

安裝 Elixir 並執行 Hello, world

從 elixir-lang.org 安裝 Elixir(同時會安裝或需要 Erlang/OTP)。腳本使用 .exs 副檔名;編譯的專案檔案則使用 .ex。iex 會啟動互動式 shell。

# 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

註解以 # 開頭。許多地方的函式引數括號是可省略的,所以 IO.puts "hi" 同樣可行。

基本型別與不可變性

Elixir 中所有資料都是不可變的。用 = 重新綁定名稱,只是讓它指向新的值,絕不會修改舊的值。字串是以雙引號撰寫的 UTF-8 二進位資料,atom 則是以冒號開頭、有名稱的常數。

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)
型別範例說明
integer42, 1_000_000任意精度
float3.1464 位元雙精度
atom:ok, :error常數;true、false 與 nil 都是 atom
string"hello"UTF-8 二進位資料;支援 #{} 插值
list[1, 2, 3]鏈結串列
tuple{:ok, 1}大小固定,連續儲存
map%{"a" => 1}鍵值儲存結構

模式比對

在 Elixir 中,= 是比對運算子。左側是模式;若右側符合該模式,模式中的變數就會被綁定。模式比對無所不在:賦值、case、函式標頭與錯誤處理都會用到。

{: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

若模式不符,Elixir 會引發 MatchError。釘選運算子 ^ 的意思是「使用這個變數目前的值」,而不是重新綁定。

流程控制

Elixir 有 case、cond 與 if/unless,而且它們都會回傳值。沒有會修改計數器的 for/while 迴圈;請改用遞迴、Enum 模組或 for 生成式。

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]

只有 false 與 nil 為假值;其他一切(包含 0 與 "")都是真值。

函式、模組與管線運算子

具名函式定義在以 defmodule 宣告的模組中。一個函式可以有多個子句,由模式比對與 guard(when)來選擇。匿名函式寫成 fn ... end,呼叫時要加點。管線運算子 |> 會把前一個運算式的結果,當成下一次呼叫的第一個引數。

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"]

函式由名稱與元數(arity,即引數個數)識別,文件中寫成 String.split/1 或 Math.factorial/1。

集合:串列、map、關鍵字串列與 Enum

Enum 模組適用於任何可列舉的集合,提供 map、filter、reduce、sort_by、group_by 等許多函式。Map 是主要的鍵值結構;關鍵字串列(如 [size: 10, color: :red])則常用來傳遞選項。

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"

擷取運算子 & 可建立簡短的匿名函式:&(&1 * 10) 等同於 fn x -> x * 10 end,而 &+/2 指的是接收兩個引數的 + 函式。

行程與並行

Elixir 的招牌特色是 BEAM 行程:一種非常輕量、彼此隔離、擁有自己記憶體的執行單位。行程之間只靠傳送訊息溝通,單一機器就能執行數十萬個行程。Supervisor 會重新啟動當掉的行程,這正是 Elixir 「let it crash」容錯作法的基礎。

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]

在實際應用中,你很少直接呼叫 spawn,而是使用 OTP 提供的 Task、GenServer、Agent 與 Supervisor 等抽象。

錯誤處理

慣用的 Elixir 對可預期的失敗會回傳帶標籤的元組,如 {:ok, value} 或 {:error, reason},再用 case 或 with 處理。名稱以 ! 結尾的函式(如 File.read!/1)則會改為引發例外。例外可用 try/rescue 攔截,但並不常見。

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 會串接多個比對,並在第一個不符的地方停下來,回傳該值。它能避免層層巢狀的 case 運算式。

完整的小程式:字詞頻率統計

這個腳本統計一段文字中的字詞,並印出最常見的三個。它是典型的 Elixir 管線,由 String 與 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

接下來可以學什麼

  • hexdocs.pm 上的官方 Getting Started 指南,以及 elixir-lang.org 網站。
  • HexDocs,Elixir 與所有 Hex 套件的文件都發布在這裡。
  • Phoenix 框架的網頁開發指南。
  • 社群免費課程 Elixir School,以及可用來練習的 Exercism Elixir 路徑。

常見問題

Elixir 與 Erlang 有什麼差別?

兩者運行在同一個 BEAM 虛擬機上,也能互相呼叫對方的程式碼。Elixir 加入了受 Ruby 啟發的語法、巨集、管線運算子,以及 Mix 與 Hex 等工具;Erlang 則是較早出現、OTP 即是以它建構的語言。

Elixir 適合開發網站嗎?

適合。Phoenix 框架速度快,能處理大量同時連線,LiveView 則讓你用伺服器渲染的 HTML 與少量 JavaScript 建立互動式頁面。

Elixir 是靜態型別嗎?

Elixir 是動態型別。近期版本正在編譯器中加入漸進式的集合論型別系統,許多團隊也會搭配 Dialyzer 與 typespec 來找出型別錯誤。

Elixir 中的 |> 是什麼意思?

|> 是管線運算子。x |> f(y) 等同於 f(x, y),所以一連串的轉換可以由上往下閱讀。

.ex 與 .exs 檔案是什麼?

.ex 檔案會作為專案的一部分被編譯,.exs 檔案則是執行時才解譯的腳本,常用於測試與設定。

全部編碼教學