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が利用できることが確認できます。
$ nix develop
erlang "27"
gleam 1.10.0プロジェクトを作成する
gleam new でプロジェクトを作成します。今回はプロジェクトを GitHub以外でホストしているので、Gleamがデフォルトで追加するGitHub用のファイルを省くために --skip-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)より新しいバージョンが要求されていました。
同じ問題に遭遇した方が、私がこのエラーに直面する わずか1時間前に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 failuresElixirパッケージをインストールする
仕様がシンプルなパッケージを選びたいので、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 は2つのパラメータを取ります。
Enumerableプロトコルを実装したオブジェクト。- (任意)
encode_options()型。
そして、Enumerable プロトコルを実装したオブジェクトを返します。
私はElixirに詳しくないので、対話型のElixirシェルである 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パッケージのドキュメントにあるサンプルを1つ試してみます。
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のシジル構文がよく分かっておらず、「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 に相当するものがないようなので、Enumerable からGleamが理解できる型へ変換するために、別のElixir APIを使う必要があります。
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仮想マシン上で動作するバイトコードにコンパイルできます。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のパイプ演算子で csv_encode に渡し、さらにパイプ演算子で結果をGleamで扱える文字列のリストに変換します。
この csv モジュールで public として宣言するのはこの関数だけです。他のラッパーは低レベルすぎて、このモジュールを利用する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標準ライブラリの Gleamのstring.concat 関数で1つの文字列に結合します。最後に 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さんに、この投稿への有益なフィードバックをいただいたことに感謝します。
記事をランダムに読む