• Gleamにマクロは必要なのか?


    「Gleamにマクロは必要か?」

    という議論はGleamコミュニティで度々話題になるテーマで、自分もGleamを好きな人間の一人なのでこれについて色々考えたりしている。

    この記事はGleamにとってマクロは必要かどうかを個人的に考察してみた内容を書いてみたもの。 いわゆるポエムってやつです。

    結論

    先に結論を言ってしまうと、Gleamにマクロは必要ない。またマクロに準ずる方法としてコード生成が良いと考える

    根拠として、以下が上げられる。

    • Gleamの精神性はRustよりGoに近いので、Goで広く用いられているコード生成の方がGleamに向いている。
    • async/await, 例外処理、middleware、デコレータはuse構文でカバー可能。
    • コンパイラの実装や負担が増えるため。
    • そもそもGleam自体がある種のマクロである。

    Gleamの精神性

    GleamはよくRustに似ていると言われる。Gleamコンパイラ自体Rustで書かれていることもあってか、キーワードなどは非常に似通っている。

    // GleamのHello, world!
    import gleam/io
    
    pub fn main() {
      io.println("Hello, world!")
    }
    // RustのHello, world!
    fn main() {
        println!("Hello, world!");
    }

    しかし、Gleamの精神性はどちらかと言うとGoの方が似ている

    と言うのも、Gleamは極力構文を少なくする方針の言語であり、Goもシンプルさを重視している言語なため。 また、並列処理に長けているという特徴も両者がより近しい存在である証左とも言える。

    use構文の活用

    Gleamにはuseという面白い構文がある。

    これは言わばコールバックの中身を外に抉り出したかのような構文で、Gleamではこの構文を使って

    • 例外処理
    • async/await
    • middleware

    などを表現している。

    例えばこれを

    try(Ok(1), fn(x) { Ok(x + 1) })
    use x <- try(Ok(1))
    Ok(x + 1)

    こう書ける。

    またuseは連鎖できるので、例えば複数のuseを積み重ねるように書ける。

    import gleam/io
    import gleam/result.{try}
    
    pub fn main() {
      use x <- try(Ok(1))
      use y <- try(Ok(2))
      Ok(x + y)
      |> io.debug
    }

    Playground

    コード中に出てくるOkはいわゆるResult型、型く言うならモナドというやつになる。 つまり、useを使うことでHaskellの>>=っぽい操作を実現できる。

    ここからが面白いところで、useの実態はなんてことないただのコールバックになっている。 つまり、最後の引数にコールバックを取る関数ならなんでもuseが適用できるし、自身が書く関数を使う際にも同様のことが可能になる。

    更に、useを使う側では受け取ったコールバックをいつ実行するかを実装者が決められる。 なのでPythonのデコレータもこれで実装できる。

    import gleam/io
    
    pub fn main() {
      // デコレートする
      use <- decorate()
      sub()
    }
    
    
    // デコレータ
    fn decorate(fun: fn () -> Nil) {
      io.println("before!")
    
      // 対象の関数を実行
      fun() 
    
      io.println("after!")
    }
    
    // デコレートする関数
    fn sub() {
      io.print("This is sub!")
    }

    Playground

    useを使うことでこれらのパターンは全て対応できるので、これらの機能を実装するためにマクロを実装するメリットはないと考える。

    コンパイラの実装や負担が増える

    ここまでは実装する必要性がない根拠について述べてきたけれど、ここからは明確なデメリットについて述べていく。

    まず、よく使われているであろう準引用型のマクロを実装する方法を考えてみる。

    このマクロを実装するにはなんらかの形でコンパイラに処理系を実装する必要がある。 Gleamで言うならGleamコンパイラにGleamのサブセットを実装するのが妥当だと思う。1

    Gleamはいわゆるトランスパイル言語に分類されるので新たに処理系を実装するのは単純にコードベースと負担が増える。 またGleamは言語仕様がとても小さく、それ単体ではほぼ何もできないため標準ライブラリ相当のモジュールも用意する必要がある。

    最近はQuickJSといった埋め込み可能なJavaScript Runtimeがあるためこれらを使う方法もあるだろうが、前述したようにメリットが薄いなかこれらの機能を追加するのは長期に渡って負担になると考える。

    そもそもGleam自体がマクロである

    そもそもマクロとはプログラムからプログラムを生成する言語機能である。 そしてGleamはGleamプログラムからErlang/JSプログラムを生成する。

    つまり、Erlang/JSの立場からGleamを見ると、GleamはGleamプログラムから別言語のプログラムを生成するある種のマクロであるという解釈が可能になる。

    そのGleamにマクロを追加するということはマクロ生成マクロを実装することに等しく、単純にプログラムの複雑化を促進してしまう。

    実は自分はこれに気付くまでマクロ推進派だった。 けれどこの事実に気付いてからはマクロ実装に対して反対の立場を取っている。 また、マクロの代替として以下の方法を考えている。

    自分が考えるマクロに変わるソリューション

    自分はGleamにマクロを実装する代わりに、Goのようなコード解析・生成機能を整えるべきだと考えている。

    この考えに至ったきっかけのプロジェクトがある。squirrelというライブラリで、これはsqlxのようにSQLを構文解析してGleamのソースコードを生成する

    生成されたソースコードはもちろん型が付いているので、生成したコードを使う際はLSPとコンパイラによる支援が得られる。 このライブラリが登場した時はコミュニティがかなり盛り上がり、自身もこのアプローチに将来性があると確信したのを覚えている。

    Gleamでコード生成する方法として、以前は暖かみのある文字列連結が多用されていた。 けれど最近はGleamでデータ構造をしっかりと作り、型安全にコードを生成するライブラリが作られている。

    この記事を書く数日前にもGleamコードでGleamを生成するライブラリが公開されていた。

    自分はこれらのアプローチを更に一歩進めて、Gleamプログラムのパースとコード生成を共通のASTで行なえるようにするべきだと考えている。

    Gleamのパーサとして、純粋なGleamで実装されたglanceというライブラリがある。

    しかしこのライブラリの逆―ASTからGleamプログラムを実装するライブラリは現時点(11/15)で確認できていない。 もしglanceが生成するASTをGleamプログラムに戻すライブラリがあったらどうだろう。

    既存のGleamプログラムから任意の操作を行なったGleamプログラムを出力できるようになる。 つまりあとライブラリがひとつあれば、プログラムからプログラムを生成できるプログラム―つまり事実上のマクロを実現できる。

    このアプローチは実装コストが少なく、純粋なGleamで実装すれば両方のターゲットで使えるためマクロの機能を実現する方法としてかなり良いと考えている。

    また、これを現実にするためにglance ASTからGleamプログラムを生成するライブラリを実装しようとも考えている。 幸いgleamgenの前例があるのでuseを使ってスコープを表現するAPIのアイデアなどを拝借しながら実装していこうと考えている。

    脚注

    1. この形式で実装しているNimにはサブセットであるNimScriptが実装されている。 ↩︎