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

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

Gleamでイテレータを扱うライブラリはyielderというライブラリに分割されている。
これは一応記事にしたので見える形にはなっているけれど、他の記事修正が追いついていないのでここでも紹介する。

gleam dev
コマンドを使うと、dev/
ディレクトリにあるモジュール(プロジェクト名)のmain関数が実行される機能がGleam1.11で追加された。
なおこの挙動はgleam test
と同じ。
このコマンドは開発時に必要なスクリプトを実行するために追加されたもので、従来パッケージの中に開発用スクリプトを配置する場合、直接プロジェクトに含めてgleam run -m
する必要があった。
この方法は、ライブラリなどをパッケージ化した際に、パッケージの中にそれらの開発用スクリプトが含まれてしまう問題があった。
dev/
ディレクトリに含まれているスクリプトはパッケージに含まれないので、これらの問題を解消できる。

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を切り分けたほうが良いかもしれない。

GleamのJavaScriptターゲットにおいて、バンドルを行うパッケージは現時点でEnderchief/esgleamのみ。
また同名のesbuildプラグインとしてbwireman/esgleamがある。(かなり紛らわしい)

Gleamでパッケージをimportするとそのパッケージの名前空間はsrc
配下が1つの名前空間に結合される。
どういうことか詳しく説明していく。
まず、fuga
とpiyo
という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のバイナリを取得できるのでより速く開発環境を構築できる。

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:)
}
このパターンのミソはここにある。
fn set_foo(config: Config, foo) {
Config(..config, foo:)
}
これはConfig
を受け取ってfoo
の値を受けとったものに変更して返すという関数。
あたかもConfig
を変更しているように見えるが、Gleamでは値は不変なため両者は異なる値として扱われる。
つまり、この関数はConfig
を受け取り新たなConfig
を返す変換関数として機能する。
入力と出力が異なるため動的に値を変更しているように見えるが、内部を見れば至って普通のデータ変更関数として実装されている事が分かる。
この構文については「レコードのアップデート」という形で公式言語ツアーでも紹介されている。

ちなみに、サンプルコードに出てきた以下の書き方はGleamの正しい構文で、ラベル省略記法と呼ばれている。
fn set_foo(config: Config, foo) {
Config(..config, foo:)
}
ラベルを伴う構文(ラベル付き引数を受け取る関数、カスタム型等)に値を指定する際、ラベル名と同じ変数がスコープ内にある場合は:
以降を省略するとコンパイラが自動で補完してくれるという挙動をする。
この構文については公式のツアーでも解説されている。