編碼 · 程式語言
Elixir 入門教學
簡答
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)| 型別 | 範例 | 說明 |
|---|---|---|
| integer | 42, 1_000_000 | 任意精度 |
| float | 3.14 | 64 位元雙精度 |
| 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)}")
endwith 會串接多個比對,並在第一個不符的地方停下來,回傳該值。它能避免層層巢狀的 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 檔案則是執行時才解譯的腳本,常用於測試與設定。