從 Gleam 呼叫 Elixir 函式庫的簡易範例
最近我一直在嘗試 Gleam 和 Elixir,作為我尋找新程式語言的過程的一部分。
Gleam 的旗艦特色之一是它可以呼叫 Elixir 程式碼與函式庫,但我找不到任何相關範例。我根據自己對 Gleam/Elixir/Erlang 生態系的初學者理解,撰寫了一個從 Gleam 專案呼叫 Elixir 函式庫的簡易範例。
安裝相依套件
在此範例中,我使用的是
- Gleam 1.10.0
- Erlang 27.3.4
- Elixir 1.18.3
您可以依自己喜歡的方式安裝這些相依套件。我使用 Nix 來管理相依套件,因此我透過建立如下的 flake.nix 來安裝所有套件。
{
description = "Dev environment for gleam-call-elixir-simple";
inputs = {
flake-utils.url = "github:numtide/flake-utils";
# 27.3.4
erlang-nixpkgs.url = "github:NixOS/nixpkgs/8406224e30c258025cb8b31704bdb977a8f1f0";
# 1.10.0
gleam-nixpkgs.url = "github:NixOS/nixpkgs/3866ad91cfc172f08a6839def503d8fc2923c603";
};
outputs = {
self,
flake-utils,
erlang-nixpkgs,
gleam-nixpkgs,
} @ inputs:
flake-utils.lib.eachDefaultSystem (system: let
erlang = erlang-nixpkgs.legacyPackages.${system}.beam27Packages.erlang;
elixir = erlang-nixpkgs.legacyPackages.${system}.beam27Packages.elixir;
gleam = gleam-nixpkgs.legacyPackages.${system}.gleam;
inotify-tools = erlang-nixpkgs.legacyPackages.${system}.inotify-tools;
in {
devShells.default =
erlang-nixpkgs.legacyPackages.${system}.mkShell
{
packages = [
erlang
elixir
gleam
inotify-tools
];
shellHook = ''
echo "erlang" $(erl -eval 'erlang:display(erlang:system_info(otp_release)), halt().' -noshell)
gleam --version
'';
};
formatter = erlang-nixpkgs.legacyPackages.${system}.alejandra;
});
}如果我執行 nix develop,會看到 erlang 和 Gleam 已可在我的 shell 中使用:
$ nix develop
erlang "27"
gleam 1.10.0建立專案
我使用 gleam new 建立專案。我將專案託管在Github 之外,因此我加入 --skip-github 來略過 Gleam 預設加入的 Github 專屬檔案:
PROJECT_NAME='call_elixir'
gleam new --name "${PROJECT_NAME}" --skip-github .支線任務:繞過 Gleam 套件錯誤
接著,我嘗試執行 gleam new 為我產生的樣板程式碼,但執行失敗:
$ gleam run
Resolving versions
Downloading packages
Downloaded 2 packages in 0.01s
Compiling gleam_stdlib
error: Incompatible Gleam version
The package `gleeunit` requires a Gleam version satisfying 1.11.0 <= v but you are using v1.10.0.問題在於 gleam new 將 gleeunit 1.4.0 加入為相依套件,但該套件相依於 Gleam 1.11.0,比我本機的 Gleam 版本(1.10.0)還要新。
有其他人也遇到了相同的問題,並在 Github 上回報了錯誤,就在我遇到這個問題的一小時前。
我可以透過強制將 Gleam 降級至 gleeunit 1.3.1 來繞過這個問題:
$ gleam add --dev [email protected]
Resolving versions
Downloading packages
Downloaded 1 package in 0.00s
Added gleeunit v1.3.1接著 gleam run 和 gleam test 就如預期般正常運作:
$ gleam run
Compiling gleeunit
Compiling call_elixir
Compiled in 0.28s
Running call_elixir.main
Hello from call_elixir!$ gleam test
Compiled in 0.01s
Running call_elixir_test.main
.
Finished in 0.006 seconds
1 tests, 0 failures安裝 Elixir 套件
我想挑選一個語意單純的套件,那麼試試 CSV 函式庫如何?
hex 套件管理器顯示最受歡迎的 CSV 套件叫做 CSV,所以我就安裝它:
$ gleam add csv
Resolving versions
Downloading packages
Downloaded 1 package in 0.00s
Added csv v3.2.2題外話:有一個 Gleam 原生的 CSV 函式庫叫做 gsv,但我改用 Elixir 函式庫,以便練習呼叫非 Gleam 的函式庫。
使用 Elixir 測試 CSV 套件
首先,我需要了解究竟該如何呼叫 CSV 套件的 API,然後才能為我需要的 API 撰寫 Elixir 包裝函式。
我想呼叫CSV.encode 函式,其簽章如下:
@spec encode(Enumerable.t(), [encode_options()]) :: Enumerable.t()因此,encode 有兩個參數:
- 實作
Enumerable協定的物件。 - (選用)
encode_options()型別。
而它會回傳一個實作 Enumerable 協定的物件。
我不懂 Elixir,所以我啟動了 iex,也就是 Elixir 的互動式 shell,來了解其語意:
iex接著,我在 iex 中安裝 CSV 套件:
iex> Mix.install([:csv])
Resolving Hex dependencies...
Resolution completed in 0.008s
New:
csv 3.2.2
* Getting csv (Hex package)現在,我來試試 CSV 套件文件中的其中一個範例:
iex> [~w(a b), ~w(c d)]
[["a", "b"], ["c", "d"]]
iex> |> CSV.encode
#Function<61.117496853/2 in Stream.transform/3>
iex> |> Enum.take(2)
["a,b\r\n", "c,d\r\n"]我還不懂Elixir 的 sigil 語法,也不喜歡用「a」和「b」的範例,所以這裡有一個對我來說更直觀的重寫版本:
iex> CSV.encode([["movie", "rating"], ["The Godfather", 10], ["Gigli", 2]])
#Function<61.117496853/2 in Stream.transform/3>
iex> |> Enum.to_list()
["movie,rating\r\n", "The Godfather,10\r\n", "Gigli,2\r\n"]
iex> |> IO.puts()
movie,rating
The Godfather,10
Gigli,2
:ok好的,看起來 CSV.encode 接收一個字串清單的清單,並回傳一個字串的 Enumerable。
為 Elixir 套件建立 Gleam 包裝
既然我已經了解 CSV.encode 的語意,現在就需要撰寫一個包裝函式,以便從 Gleam 呼叫它。
為 Elixir 函式撰寫 Gleam 包裝函式的主要挑戰在於,這兩種語言的型別並不相符。Gleam 使用較為嚴格的靜態型別,而 Elixir 則使用較具彈性的動態型別。
包裝 CSV.encode
我想呼叫 CSV.encode 函式,其簽章如前所述如下:
@spec encode(Enumerable.t(), [encode_options()]) :: Enumerable.t()看起來Gleam 標準函式庫中並沒有任何等同於 Elixir Enumerable 的東西,所以我需要使用另一個 Elixir API,將 Enumerable 轉換為 Gleam 能理解的東西。
Enum.to_list 似乎是最好的選擇,因為它會回傳 Elixir 內建的 list 型別,而 Gleam 也有對應的 List 型別。
所以,首先我將為 Elixir 的 CSV.encode API 定義一個 Gleam 包裝函式:
// Create a custom Gleam type to represent the type we receive from Elixir.
type ElixirEnumerable
@external(erlang, "Elixir.CSV", "encode")
fn csv_encode(data: List(List(String))) -> ElixirEnumerable@external 屬性讓我能從 Gleam 呼叫 Elixir 程式碼。Gleam、Elixir 和 Erlang 都可以編譯成可在BEAM virtual machine(BEAM 虛擬機器)上執行的位元組碼。在 BEAM 中,CSV.encode 函式出現在命名空間 Elixir.CSV 底下,所以這就是為什麼我需要在 @external 屬性中指定 Elixir 的原因。
我將輸入參數定義為 Gleam 的字串清單的清單(List(List(String))),它與 Erlang 的 Enumerable 型別相容。
CSV.encode 會回傳 Elixir 的 Enumerable,但我不知道 Gleam 中有對應的型別,所以我定義了一個自訂型別 ElixirEnumerable。
Gleam 程式碼無法對 ElixirEnumerable 做任何事,因為它沒有任何 Gleam 能原生存取的資料,所以我需要一種方法將 ElixirEnumerable 轉換為 Gleam 原生的型別。
將 Elixir 的 Enumerable 轉換為 Gleam 的 List
為了將 Elixir 的 Enumerable 型別轉換為 Gleam 的 List,我宣告了另一個用於轉換結果的外部函式:
@external(erlang, "Elixir.Enum", "to_list")
fn enum_to_list(elixir_enum: ElixirEnumerable) -> List(String)在這裡,我使用 Elixir 的Enum.to_list 函式將 ElixirEnumerable 轉換為Elixir List 型別,它似乎與Gleam List 型別相符。
建立 Gleam 友善的包裝函式
既然我已經撰寫了可以包裝我所需 Elixir API 的函式,現在是時候將它們整合起來,透過這個模組向我的 Gleam 應用程式公開一個函式:
pub fn encode(data: List(List(String))) -> List(String) {
data
|> csv_encode
|> enum_to_list
}這個函式很簡單。它接收字串清單的清單形式的資料,然後使用Gleam pipe operator(管線運算子)將其傳遞給 csv_encode,再一次使用 pipe operator 將結果轉換為 Gleam 相容的字串清單。
這是我在 csv 模組中唯一宣告為公開的函式,因為其他包裝函式太過底層,對此模組的 Gleam 呼叫端沒有實用價值。
完整的模組如下所示:
// Create a custom Gleam type to represent the type we receive from Elixir.
type ElixirEnumerable
@external(erlang, "Elixir.CSV", "encode")
fn csv_encode(data: List(List(String))) -> ElixirEnumerable
@external(erlang, "Elixir.Enum", "to_list")
fn enum_to_list(elixir_enum: ElixirEnumerable) -> List(String)
pub fn encode(data: List(List(String))) -> List(String) {
data
|> csv_encode
|> enum_to_list
}呼叫我的包裝函式
既然我已經有了一個 Gleam 原生的 CSV.encode 函式包裝,現在我可以撰寫一個簡單的 Gleam 應用程式來呼叫我的包裝函式:
import gleam/io
import gleam/string
import csv
pub fn main() {
[
["Title", "Author", "Release Year"],
["Infinite Jest", "David Foster Wallace", "1996"],
["Emma", "Jane Austen", "1815"],
["Catch-22", "Joseph Heller", "1961"]
] |> csv.encode()
|> string.concat()
|> io.print()
}我建立了一個簡單的字串清單的清單,並將其傳遞給我的 csv.encode Gleam 包裝函式。它會回傳一個字串清單,因此我呼叫Gleam 的 string.concat 標準函式庫函式將字串串接成單一字串。最後,我呼叫 io.print 將結果印到主控台。
$ gleam run
Compiling call_elixir
Compiled in 0.20s
Running call_elixir.main
Title,Author,Release Year
Infinite Jest,David Foster Wallace,1996
Emma,Jane Austen,1815
Catch-22,Joseph Heller,1961成功了!我已成功從我的簡易 Gleam 應用程式呼叫了 Elixir CSV 函式庫。
原始碼
此範例的完整原始碼如下:
–
感謝 Louis Pilford(路易斯·皮爾福德)與 DisguisedPigeon 對本文提供的寶貴回饋。
隨機一篇部落格