はじめに
この記事について
Gleam というまだ若いプログラミング言語を最近知った(2年前にブクマしてたんだけど忘れていた)ので、どんな感じなのかちょろっと触ってみました。
Gleam とは
非常に簡潔ながら Rust っぽい文法を持つ関数型プログラミング言語です。Erlang VM 上で動くので、Erlang や Elixir のモジュールを組み込むことができるほか、Erlang と JavaScript にトランスパイルすることができます。
新しいプログラミング言語に出会うと、とりあえず動かしてみるのが趣味なんですが(新しいラーメン屋ができたらとりあえず行って食べてみる感覚で)、Gleam からは非常に満足する第一印象を得られました。
私は Nim と Clojure がとくに好きなんですが(Elixir も好き)、その2つに比肩するのではないかという大ヒットです(何年ぶりだろうこの感覚…)。
後述しますが、他の言語ではあまり見かけないようなちょっと変わったところもあり、クセになる味といった感じの趣です。
下記のサイトを見るとほとんどのことはわかるので、気になった方は見に行ってみてください。
お断り
Gleam はまだ若い言語であるため、シンタックスハイライトに未対応です。とはいえコードが読みにくいのもアレなので、本記事では文法の似ている Rust のシンタックスハイライトを借りて表示しています。ご了承ください。
目次
- はじめに
- 目次
- Gleam の主な特徴
- 1. 関数型である(関数が第一級オブジェクトである)
- 2. 変数はイミュータブル、再束縛不可
- 3. ユニークな型定義
- 4. if/else, for, while などがない
- 5. Erlang や JavaScript の関数を使える
- 所感
Gleam の主な特徴
- 関数型である(関数が第一級オブジェクトである)
- 変数はイミュータブル、再束縛不可
- ユニークな型定義
- if/else, for, while などがない
- Erlang や JavaScript の関数を使える
以下、各項目について書きます。
1. 関数型である(関数が第一級オブジェクトである)
関数型の定義にはバリエーションがあるかと思いますが、ここでは「関数が第一級オブジェクトである」ということにします。つまり、関数を変数に入れられる、引数に取れる、ということです。他にも、関数合成やカリー化を標準ライブラリに備えています。
import gleam/function import gleam/int import gleam/io pub fn run() { let increment = fn(x) { x + 1 } let double = fn(x) { x * 2 } let calc = function.compose(increment, double) let result = calc(10) io.println(int.to_string(result)) // output: 22 let add = fn(x, y) { x + y } let add_n = std_function.curry2(add) let result = [1, 2, 3] |> list.map(add_n(1)) list.map(result, int.to_string) |> string.join(", ") |> io.println // output: 2, 3, 4 }
return
キーワードはなく、最後に評価された式の結果が返り値になります。
関数型言語に必須(?)のパイプ演算子も標準で備わっています、見やすさ・書きやすさが段違いですね。
2. 変数はイミュータブル、再束縛不可
代入か束縛か、というのは言語によって異なりますが、Gleam の場合は束縛です。理解が曖昧ですが、束縛は基本的にイミュータブルで変更不可だと思うので、再束縛不可というのは自家撞着ですが、同じ変数に別の値を複数回結びつけることができない、ことを強調したいのであえて「再束縛不可」と書きました。
細かい話はおいといて、コードを見てみると以下のような違いがあります。
代入するタイプの言語だと以下のコードはOKそうですが、Gleam ではコンパイルエラーになります。
let n = 10 io.println(int.to_string(n)) // 10 n = n + 1 io.println(int.to_string(n)) // 上の行でコンパイルエラー
上のようなケースでは再び let
を使って別の(同名でも可)変数に束縛します。
let n = 10 io.println(int.to_string(n)) // 10 let n = n + 1 io.println(int.to_string(n)) // 11
3. ユニークな型定義
これは最初面食らったんですが、ユーザー定義型を作るときに列挙型っぽい作りにできるのがユニークだなと思いました。
一般的な直積型の型だとこんな感じです。
app/user.gleam
import gleam/int pub type User { User(id: Int, name: String, email: String) } pub fn to_string(user: User) { "User{id: " <> int.to_string(user.id) <> ", name: " <> user.name <> ", email: " <> user.email <> "}" }
オブジェクトの各フィールドには .
でアクセスします。
app.gleam
import gleam/io import app/user pub fn main() { let alice = user.User(1, "Alice", "alice@example.com") io.println(user.to_string(alice)) }
ファイル名がそのままモジュール名になって、型や関数を呼び出すときは module
+ .
を付けて呼び出します。フィールドアクセスと書き方が同じなのでじゃっかん読みづらさはありますが、慣れの問題かと思います。
上の書き方だとコンストラクタを一つ持つクラスっぽいですよね。でも、Gleam ではコンストラクタを複数書くことで、列挙型を定義することができます。
実際、Rust では enum で定義されている Option 型が Gleam では以下のように定義されています。
pub type Option(a) { Some(a) None }
型定義はジェネリクスっぽくも使えます(要は any)。専用の記法(よく見る <>
で囲まれてるやつ)がないので、最初ちょっと混乱したんですが、上の a
は型アノテーションです。
一部のみ特定の型を指定することもできます。下記はキーを文字列で固定したキーバリューペアです。any で指定した値はなんでも入れられるわけではなく、同型の値しか入れることはできません。
app/key_value_store.gleam
pub type KeyValueStore(any) { KeyValueStore(items: Map(String, any)) } pub fn from_list(items: List(#(String, a))) -> KeyValueStore(a) { KeyValueStore(map.from_list(items)) } pub fn get(store: KeyValueStore(a), key: String) -> Result(a, Nil) { map.get(store.items, key) }
#(String, a)
はタプルです。
app.gleam
import gleam/io import app/user import app/key_value_store as kvs pub fn main() { let alice = user.User(1, "Alice", "alice@example.com") let store = kvs.from_list([#("alice", alice)]) let alice = kvs.get(store, "alice") io.debug(alice) // Ok(User(1, "Alice", "alice@example.com")) }
Ok
は Rust でもおなじみの Result
型で、Ok
または Error
が返ります。
4. if/else, for, while などがない
ループがないのは関数型なのでさもありなんですが、if/else までないのはちょっと驚きました。条件分岐は case
式で行います。
Result 型の値が返ってきた場合、以下のように条件分岐できます。
let alice = kvs.get(store, "alice") // Result(User, Nil) case alice { Ok(user) -> io.println(user.to_string(user)) Error(_) -> io.println("alice not found") } // User{id: 1, name: Alice, email: alice@example.com}
余談ですが、Ok(user)
の部分はデストラクチャで、オブジェクトからフィールドの値を取り出すことができます。
let user.User(id, name, email) = alice io.println("id=" <> int.to_string(id) <> ", name=" <> name <> ", email=" <> email) // id=1, name=Alice, email=alice@example.com
閑話休題。
case 式は値を返すので、以下のように書くこともできます。
io.println(case alice { Ok(user) -> user.to_string(user) Error(_) -> "alice not found" }) // またはパイプ演算子を使う case alice { Ok(user) -> user.to_string(user) Error(_) -> "alice not found" } |> io.println
case 式は各アームで同じ型の値を返す必要があるので、エラーのときはなにも出力したくない場合は最初の書き方にするしかなさそうです。
case alice { Ok(user) -> io.println(user.to_string(user)) Error(_) -> Nil }
5. Erlang や JavaScript の関数を使える
Erlang VM 上で動くので、Erlang や Elixir の資産が使えるのはわかるんですが、JavaScript まで使えるとは驚きました。
以下のような JavaScript モジュールを置いておきます。
src/module.mjs
export function now() { let now = new Date(); return now.toISOString(); }
Gleam 側から以下のように呼び出します(本当は .mjs ファイルだけ別の場所に配置したかったんですが、ビルド時になぜかコピーされなかったので仕方なく src 直下に配置しました)。
src/app/js.gleam
import gleam/io pub fn run() { let now = now() io.println("now: " <> now) } @external(javascript, "../module.mjs", "now") pub fn now() -> String
JavaScript として実行すると、
$ gleam run --target javascript Compiled in 0.01s Running app.main now: 2023-11-05T11:55:20.196Z
と出力されます。デフォルトでは Node.js が呼ばれるようです。本記事執筆時点では node 以外では deno が使えました。
$ gleam run --target javascript --runtime deno
所感
文法が Rust っぽいところが書きやすく、かつ変数はすべてイミュータブルで所有権もないので、書いていてストレスはほぼ感じません。型アノテーションは任意なので、気を抜くと書き忘れるんですが、型が自明なところを省略できるのはかえってメリットなのかな、という気もします。
言語仕様がシンプルで覚えることも少ないので、すぐに書くスピードが上がります。逆に、シンプルすぎるがゆえにマクロがほしいと思うところもありましたが、まだメジャーバージョンが 0 な若い言語なので、今後どんな風に言語仕様を整えていくのか、非常に楽しみです。
Nim にしろ Clojure にしろ Elixir にしろ、私が気に入る言語はメジャーになったことがないので、Gleam もマイナー言語として成長していくんだろうと思いますが、開発陣のみなさんにはがんばってほしいですね。