Open12

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/