Gleamでおもしろかった話をまとめる

3年前くらいにGleamを触ったきりだったが、最近version 1がリリースされてずっと触ろうと思って結構時間が経ってしまった。最近ドキュメントを眺めていてなんとなくおもしろそうだと再度思い、実際にいくつかコードを書いてみておもしろかったので、おもしろかった機能をまとめておく。
現状だが、Language Serverは結構動く。エラーメッセージもかなり改善されてきており、わかりやすくなっている。普通に開発をしていて妙なバグに直面することはない。一方で新しい言語であるため、詰まったときの情報がかなり少ない。このスクラップも、詰まったときに役立つように残しておく。

ちょっと昔に軽く触っているので完全にゼロからのスタートかというと難しいけど、RustをやっていてかつScalaをやっていたからか、1日でスラスラ書けるようになってしまった。見てくれはFunctional Programming flavourなRustなので、FPがわかっていてRustが書ければすぐにアプリケーション作れて楽しいと思う。

ジェネリクスは丸括弧
Option
型の実装を見て一瞬戸惑うのだが、(a)
はa
という型引数らしい。Gleamでは、ジェネリクスは(type)
で記述される。たとえば他の言語でいうList<A>
はList(a)
と記述される。
一瞬、type
に持ちうる型に共通するフィールドかと思ったが、型注釈が付いていないので判別可能ではある。
pub type Option(a) {
Some(a)
None
}

Nil
Gleamによるサンプルを見ると登場する謎のNil
だが、これは他の言語でいうところのユニット型を指す。
Gleamではたとえば次のようにmain関数の戻り値として登場することがある。
pub fn main() -> Nil {
todo
}
Gleamの関数は必ずなにかの値を返す必要がある。なのでこの関数は、Nil
という値を返していることになる。他の言語ではnull
に近しい概念として用いられるが、これを表現したい場合GleamにはOption
がある。

パイプ演算子
Gleamでは|>
でパイプ演算子が利用できる。一部のプログラミング言語で見かけることのある便利な機能だと思う。下記は、パイプ演算子でファイルの読み込みを実装してみた例である。
/// Reads a file and returns its contents as a list of lines
/// Returns an error message if the file cannot be read
fn read_file(path: String) -> Result(List(String), String) {
simplifile.read(path)
|> result.map(fn(content) { string.split(content, "\n") })
|> result.map_error(fn(e) {
string.inspect(e)
|> string.append(": File Not Found")
})
}
パイプ演算子は、第一引数の型があっていれば利用できる。たとえば、simplifile.read
を呼び出すとResult
型の値が返ってくる。これをresult.map
に繋げて型の変換を行っている。result.map
は次のような定義になっており、第一引数の値がパイプによって与えられていることがわかる。
pub fn map(over result: Result(a, e), with fun: fn(a) -> b) -> Result(b, e) {
case result {
Ok(x) -> Ok(fun(x))
Error(e) -> Error(e)
}
}
ちなみに、関数の引数にさらについているover
やwith
はLabelled Argumentsという機能でこれもGleamに独特な機能な気がする。
あんまりよく知らないのだが、Elixir由来なのだろうか。

標準ライブラリは結構薄め?
stdlibがかなり薄い気がする。何をやるにもサードパーティ製のライブラリを利用する必要がある。下記に、何が標準ライブラリに入っているかのリストがある。
最近grepを実装したみたのだけど、ファイルの読み書きすら simplifile
というサードパーティ製のライブラリに頼る必要があった。

Gleamにifはない
ない。case
を代わりに使う。
♥ ❯ gleam build
error: Syntax error
┌─
│
52 │ if
│ ^^ Gleam doesn't have if expressions
If you want to write a conditional expression you can use a `case`:
case condition {
True -> todo
False -> todo
}
See: https://tour.gleam.run/flow-control/case-expressions/

if
を「ない」と案内するなら、for
やwhile
、loop
に対しても同様に案内を出すべきな気がしてしまう。

@external
simplifileというファイルIOのライブラリを見ていると、次のような記述に出会うことがある。
@external(erlang, "simplifile_erl", "create_directory")
@external(javascript, "./simplifile_js.mjs", "createDirectory")
pub fn create_directory(filepath: String) -> Result(Nil, FileError)
これはおそらくだがシステムコールを利用する機能を実装する際に利用されるものと思われる。たとえば、simplifile_js.mjs
のcreateDirectory
関数を見ると次のようにNode.jsの機能を呼び出していることがわかる。
export function createDirectory(filepath) {
return gleamResult(() => fs.mkdirSync(path.normalize(filepath)));
}
Erlang側は次のように実装されている。
%% Create a directory at the given path. Missing parent directories are not created.
create_directory(Dir) ->
posix_result(file:make_dir(Dir)).
Gleamは結局のところ、ErlangかJavaScriptのランタイムの上のラッパー言語という感触に近い。したがってサードパーティライブラリでOSの機能を呼び出す機構を実装する場合、たとえばErlangの知識が必要になる。本格的にいろいろ直したいとか、ライブラリを作りたいとなった場合、ErlangもNode.jsもわかっていないとダメ、ということか…。