nunulkのプログラミング徒然日記

毎日暇なのでプログラミングの話題について書きたいことがあれば書いていきます。

Gleam の第一印象

はじめに

この記事について

Gleam というまだ若いプログラミング言語を最近知った(2年前にブクマしてたんだけど忘れていた)ので、どんな感じなのかちょろっと触ってみました。

Gleam とは

gleam.run

非常に簡潔ながら Rust っぽい文法を持つ関数型プログラミング言語です。Erlang VM 上で動くので、Erlang や Elixir のモジュールを組み込むことができるほか、ErlangJavaScript にトランスパイルすることができます。

新しいプログラミング言語に出会うと、とりあえず動かしてみるのが趣味なんですが(新しいラーメン屋ができたらとりあえず行って食べてみる感覚で)、Gleam からは非常に満足する第一印象を得られました。

私は Nim と Clojure がとくに好きなんですが(Elixir も好き)、その2つに比肩するのではないかという大ヒットです(何年ぶりだろうこの感覚…)。

後述しますが、他の言語ではあまり見かけないようなちょっと変わったところもあり、クセになる味といった感じの趣です。

下記のサイトを見るとほとんどのことはわかるので、気になった方は見に行ってみてください。

https://gleam.run/book/tour/

お断り

Gleam はまだ若い言語であるため、シンタックスハイライトに未対応です。とはいえコードが読みにくいのもアレなので、本記事では文法の似ている Rust のシンタックスハイライトを借りて表示しています。ご了承ください。

目次

Gleam の主な特徴

  1. 関数型である(関数が第一級オブジェクトである)
  2. 変数はイミュータブル、再束縛不可
  3. ユニークな型定義
  4. if/else, for, while などがない
  5. ErlangJavaScript の関数を使える

以下、各項目について書きます。

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. ErlangJavaScript の関数を使える

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 もマイナー言語として成長していくんだろうと思いますが、開発陣のみなさんにはがんばってほしいですね。