Open22

Gleamで知ってること全部書く

ピン留めされたアイテム
こまもか🦊こまもか🦊

最近なかなか記事が書けていないので、恐らく僕しか認知していないであろうGleamの事柄を見える形に書き出すためにこのスクラップを作成した。
このスクラップは新しい情報が入ったら定期的に更新していく。

こまもか🦊こまもか🦊

gleam devコマンドを使うと、dev/ディレクトリにあるモジュール(プロジェクト名)のmain関数が実行される機能がGleam1.11で追加された。
なおこの挙動はgleam testと同じ。

このコマンドは開発時に必要なスクリプトを実行するために追加されたもので、従来パッケージの中に開発用スクリプトを配置する場合、直接プロジェクトに含めてgleam run -mする必要があった。

この方法は、ライブラリなどをパッケージ化した際に、パッケージの中にそれらの開発用スクリプトが含まれてしまう問題があった。

dev/ディレクトリに含まれているスクリプトはパッケージに含まれないので、これらの問題を解消できる。

https://gleam.run/news/gleam-javascript-gets-30-percent-faster/

こまもか🦊こまもか🦊

GleamのJavaScriptターゲットはNode.js, Deno, Bunに対応しているが、それらのランタイムで切り替えられるような構文(external的な)は存在しない。

よってGleamのJavaScript系ライブラリは、処理を全てNode.js向けに書くことでそれぞれのランタイムの向けの分岐を省きpolyfillによって対応させている事が多い。

こまもか🦊こまもか🦊

JavaScriptターゲットのFFIは(現代のJavaScriptランタイムではもっぱらESMが主流なため)ESMとして定義される事が多い。

基本的にGleamは出力したJSのTree-shakingを行わないため、該当する関数が存在するFFIモジュールは全て読み込まれる。

この挙動は例えば、ブラウザ向けの環境でGleamを動かす際、Node.jsの組み込みライブラリがないエラーなどを引き起こす。

これを避けるためには、単純にNode.js向けのFFIは別のモジュールとして切り分けてやれば良い。

こうすればGleamが生成したJSコードにそれらの関数が含まれたモジュールがimportされなくなるので、これらの問題を解決できる。

もし新規でJavaScriptのFFIが関わり、かつランタイム固有の機能を使うGleamパッケージを書く際は、ffi.deno.mjs, ffi.nide.mjsのようにランタイム毎にFFIを切り分けたほうが良いかもしれない。

こまもか🦊こまもか🦊

直接は関係ないが、FFIの分割についてはplinthというNode.jsとブラウザ向けのAPIをwrapしているライブラリのディレクトリ構成が参考になる。

こまもか🦊こまもか🦊

Gleamでパッケージをimportするとそのパッケージの名前空間はsrc配下が1つの名前空間に結合される。

どういうことか詳しく説明していく。

まず、fugapiyoという2つのパッケージがあるとして、それらのsrc配下が以下のようになっているとする。

fuga

.
└── src
    └── hoge
        └── fuga

piyo

.
└── src
    └── hoge
        └── piyo

これらのパッケージをimportすると以下のようにしてアクセスできる。

import hoge/fuga
import hoge/piyo

fugaとpiyoは別々のパッケージなのにも関わらず、同じhogeという名前空間に存在しているかのように見える。

この挙動を上手く使うと、パッケージを分割しつつ、同一パッケージからimportしているかのように見える体験を提供できる。
つまり、既に開発したパッケージに対して「オプショナルなモジュール」を提供できる。

実際の例

実際に用いられている例として、最近リリースされたlustre/portalを取り上げる。

このライブラリはLustreコンポーネントを外部のDOMへと転送するパッケージとなっている。

このパッケージ自体はLustreとは別のパッケージおよび別リポジトリで開発されているが、パッケージの本体がsrc/lustre/portalに存在するため、import lustre/portalとしてimportできる。

この挙動の面白い点として、元のパッケージの作者以外でも容易に同じ名前空間に対してモジュールを注入できるという点。

例えば僕がLustreに対して、機能Aを実装したモジュールを追加したいとする。
その際には別にLustreにPRを出さずとも、src/lustre/future_aという機能を持つパッケージを作成して公開すれば良い。

そうすれば、lustre/future_aからimport出来るので、あたかも僕が作成したパッケージがLustreに含まれているかのように見える。

ここまでの説明で、「これ名前空間汚染されないの?」と思った方もいると思う。
結論として名前空間は汚染されない。
ただ、この手法には1つ気をつけなければならない事がある。

Gleamの名前空間はモジュール名の末尾しか見ないので、もしhoge/piyo/moduleというモジュールとfoo/bar/moduleというライブラリがある場合、両者は同一モジュールとして扱われ、コンパイルエラーが発生する。

これを回避するには、as句を用いて名前を修飾する必要がある。

例えば、以下のように書けばコンパイルが通る。

import hoge/piyo/module as piyo_module
import foo/bar/module as bar_module

これはまぁまぁ面倒なので、個人的にはモジュール名に関してはあまり汎用的な名前を付けないほうが良いと考えている。

こまもか🦊こまもか🦊

これは宣伝だけれども、Nixでコンパイル済のGleamバイナリを取得するoverlayを書いた。
これを使うとコンパイルをスキップしてGleamのバイナリを取得できるのでより速く開発環境を構築できる。

https://github.com/Comamoca/gleam-overlay

こまもか🦊こまもか🦊

Gleamでは設定する要素のあるパッケージ(SQLクライアント、フレームワーク等)において、Configという値を用意しその型に対して値を操作する関数を実装しユーザーに提供するという慣用的なプログラムがよく見られる。

let myconfig = config.new()
|> set_hoge("hoge")
|> set_fuga("fuga")

個人的にはこれをConfigパターンと呼んでいて、以下のように実装できる。

pub type Config {
 Config(foo: String, bar: Int, baz: Bool)
}

pub fn main() {
  let myconfig = new()
    |> set_foo("Arisu")
    |> set_bar(141)
    |> set_baz(True)
    
  echo myconfig.foo
  echo myconfig.bar
  echo myconfig.baz
}

fn new() { 
  Config(foo: "", bar: 0, baz: False)
}

fn set_foo(config: Config, foo) {
  Config(..config, foo:)
}

fn set_bar(config: Config, bar) {
  Config(..config, bar:)
}

fn set_baz(config: Config, baz) {
  Config(..config, baz:)
}

Playground

このパターンのミソはここにある。

fn set_foo(config: Config, foo) {
  Config(..config, foo:)
}

これはConfigを受け取ってfooの値を受けとったものに変更して返すという関数。
あたかもConfigを変更しているように見えるが、Gleamでは値は不変なため両者は異なる値として扱われる。

つまり、この関数はConfigを受け取り新たなConfigを返す変換関数として機能する。
入力と出力が異なるため動的に値を変更しているように見えるが、内部を見れば至って普通のデータ変更関数として実装されている事が分かる。

この構文については「レコードのアップデート」という形で公式言語ツアーでも紹介されている。

https://tour.gleam.run/data-types/record-updates/

こまもか🦊こまもか🦊

ちなみに、サンプルコードに出てきた以下の書き方はGleamの正しい構文で、ラベル省略記法と呼ばれている。

fn set_foo(config: Config, foo) {
  Config(..config, foo:)
}

ラベルを伴う構文(ラベル付き引数を受け取る関数、カスタム型等)に値を指定する際、ラベル名と同じ変数がスコープ内にある場合は:以降を省略するとコンパイラが自動で補完してくれるという挙動をする。

この構文については公式のツアーでも解説されている。
https://tour.gleam.run/functions/label-shorthand-syntax/

こまもか🦊こまもか🦊

プロジェクトを作る時にgleam new .と実行するとカレントディレクトリの名前でプロジェクトが作成される。

Gleamのプロジェクトの命名には以下の規則がある。

  • gleam_というprefixはGleam公式しか使えない
  • プロジェクト名は半角英小文字と数字、_のみ使える
  • プロジェクト名の先頭に_と数字は使えない
こまもか🦊こまもか🦊

Gleamのドキュメント化されていないサブコマンドとしてgleam compile-packageがある。
このコマンドは特定のパッケージを指定されたライブラリパスとビルド先を指定できる。

gleam compile-package --target <target> --package <path> --lib <lib_path> --out <output_dir>

ドキュメント化されてない内部コマンドとして存在しているが、mix_gleamなどで使われているため頻繁な変更はされないと思う。

こまもか🦊こまもか🦊

map関数で表現できない処理として、「前の状態を保ったまま配列を回す」というものがある。
このようなケースの実装には関数型言語でおなじみのfoldを使う。

以下のケースでは、numは始めは0なのに、処理の終了時にはnumが6になっている。

これを見ると、あたかもnumがmutableな変数に見えるが実際のところnumは不変で、処理が実行される度に新たな値で束縛されたnumが作成されている。
この普遍性はGleamのみならず、関数型言語において一般的な特徴となっている。

let items = [1, 2, 3]
  let num = 0
  list.fold(items, num, fn(num, item) {
    num + item
  })
  |> echo // 6

Playground

こまもか🦊こまもか🦊

似た関数としてreduceもある。
こちらは言語によっては同じ関数として扱われることもあるが、Gleamにおいては初期値をとらないfoldのような関数として定義されている。

初期値をとらないため、初期値のかわりにListの値を使用する。
ただ、リストが空の場合初期値が取れず処理を実行できないためエラーとなる。

また、リストの最初の要素を初期値として使用するという特性上、出力する値の型がリストの要素と同一になる。

この2点がfoldと異なるところで、双方を比べるとfoldの方が汎用的と言える。

こまもか🦊こまもか🦊

以下2つの処理は等価。
後者の方が可読性が高いため、後者の方が推奨される。

import gleam/option.{None, Some}

pub fn main() {
let foo_option = Some("foo")
  let _baz =
    foo_option
    |> option.map(fn (foo) {
      foo <> "bar"
    })
    |> option.unwrap("")
    |> echo
  
  let _baz = case foo_option {
    None -> ""
    Some(foo) -> foo <> "bar"
  }
  |> echo
}

Playground

こまもか🦊こまもか🦊

Gleamのシングルバイナリ化について、これまでJavaScriptターゲットに関しては実現できていたが、Erlangターゲットは依然として未解決の問題だった。

妥協案として、escriptと呼ばれるErlangをスクリプトとして実行する手法を用いて単一の実行ファイルとしていたが、この方法では実行するコンピューターにErlangがインストールされている必要があり、ポータビリティが損なわれていた。

そんな中、最近nix bundleというパッケージを見つけた。これはNixのパッケージを単一の実行ファイルとしてバンドルするもの。

これを用いれば、LinuxとMac限定とは言えErlangターゲットのGleamプロジェクトを単一の実行ファイルにできる。

実行ファイルにする方法もいくつか選べるが、AppImage形式が可搬性が高く、実行時のオーバーヘッドも少ないため、これをオススメする。

この方法を用いることで、GleamをCLIツールの開発言語として選びやすくなったと考えている。

詳細なビルド方法については近いうちに記事にしてまとめていきたい。

こまもか🦊こまもか🦊

最近GleamでNostrの署名を行うためのライブラリを実装している。

メンテナンスコストを抑えるため、Zigの標準ライブラリにある楕円曲線を操作するモジュールを使用してBIP340準拠の署名を行えるライブラリを作り、それをzigler経由Elixirライブラリにした。

このライブラリをFFI経由で呼び出したサンプルコードが以下のURLにある。

注意点として、GleamコンパイラはElixirプロジェクトをrebar3プロジェクトとしてコンパイルする。
そのため、zigコンパイラを取得するためのmixタスクzigler.getは実行できない。Zigをあらかじめインストールし、環境変数ZIGLER_ZIG_EXEにインストールパスを指定する必要がある。

https://github.com/Comamoca/sandbox-gleam/tree/main/ex_gleam_secp256k1

こまもか🦊こまもか🦊

アドカレなにも出せてないのでこれを1日目にします。


Gleamはcaseのガード節に関数呼び出しを使えません。
つまり、ガード節の条件を動的に変更できません。これはErlangの制約によるものです。

case thing {
  func(val) -> // thingがfunc(val)に一致したか判定したい
}

しかし、動的な条件に一致したかを判定したくなるときが往々にしてあります。
その際は以下のように関数の実行結果をcaseの値として含めます。

case thing, func(val) {
  "foo", func(val)の実行結果 -> // thingがfunc(val)に一致したか
}