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を実行すると、シェルでerlangとGleamが利用可能になっていることが確認できます。
$ nix develop
erlang "27"
gleam 1.10.0プロジェクトの作成
gleam newを使ってプロジェクトを作成します。プロジェクトはGithubの外部でホストしているので、デフォルトで追加される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余談ですが、gsvというGleamネイティブのCSVライブラリもありますが、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パッケージのドキュメントにある例の一つを試してみます。
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ラッパーを書く上での主な課題は、2つの言語で型が一致しないことです。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氏に、この投稿への有益なフィードバックをいただいたことに感謝します。
記事をランダムに読む
コメント
ログインしてコメントする