A Simple Example of Calling an Elixir Library from Gleam

Michael Lynch

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

flake.nixをダウンロード

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 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

余談ですが、gsvというGleamネイティブのCSVライブラリもありますが、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パッケージのドキュメントにあるの一つを試してみます。

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関数を使って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氏に、この投稿への有益なフィードバックをいただいたことに感謝します。

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

コメント