A Simple Example of Calling an Elixir Library from Gleam

Michael Lynch

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;
    });
}

flake.nixをダウンロード

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 rungleam 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 は2つのパラメータを取ります。

  1. Enumerable プロトコルを実装したオブジェクト。
  2. (任意) 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関数を使って ElixirEnumerableElixirの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.gleamをダウンロード

ラッパー関数を呼び出す

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()
}

call_elixir.gleamをダウンロード

文字列のリストのリストをシンプルに作成し、自作の 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さんに、この投稿への有益なフィードバックをいただいたことに感謝します。

原文は Michael Lynch により に公開されました。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。