從 Gleam 呼叫 Elixir 函式庫的簡單範例
原文由 Michael Lynch 于 發布,訂閱此部落格
最近為了尋找新的程式語言,我稍微玩了一下 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,就可以在 shell 中看到 erlang 和 Gleam 已經就緒:
$ 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 上回報了這個 bug。
我可以透過強制讓 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,但為了練習呼叫非 Gleam 的函式庫,我這裡還是選擇使用 Elixir 的版本。
用 Elixir 測試 CSV 套件
首先,我得先弄懂要怎麼呼叫 CSV 套件的 API,然後才能為我需要的 API 寫一個 Elixir 包裝層。
我想呼叫CSV.encode 函式,它的簽名如下:
@spec encode(Enumerable.t(), [encode_options()]) :: Enumerable.t()所以,encode 有兩個參數:
- 實作
Enumerable協定的物件。 - (選用)
encode_options()型別。
而且它會回傳一個實作 Enumerable 協定的物件。
我不懂 Elixir,所以我啟動了互動式 Elixir shell iex 來了解它的運作方式:
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 虛擬機器上執行的 bytecode。在 BEAM 中,CSV.encode 函式是出現在 Elixir.CSV 這個命名空間底下,所以這就是為什麼我需要在 @external 屬性中指定 Elixir。
我將輸入參數定義為 Gleam 的字串串列的串列(List(List(String))),這與 Erlang 的 Enumerable 型別相容。
CSV.encode 會回傳 Elixir 的 Enumerable,但我不知道 Gleam 有什麼對應的型別,所以我定義了一個自訂型別 ElixirEnumerable。
Gleam 的程式碼無法直接處理 ElixirEnumerable,因為它沒有任何 Gleam 能原生存取的資料,所以我需要一個方法把它轉換成 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 運算子把它傳給 csv_encode,再用一次 pipe 運算子把結果轉換成 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
}呼叫我的包裝函式
既然已經有了 CSV.encode 函式的 Gleam 原生包裝,我就可以寫一個簡單的 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 對本文提供的寶貴回饋。
隨機一篇部落格
留言
登入後參與討論