◆ 静的型付けの関数型言語
◆ Erlang または JavaScript にコンパイルしてそれらのランタイムで動かす
◆ シンプルで使いやすそうな気がする

Gleam という言語が今月の頭に 1.0 リリースを迎えてました
かわいい星のキャラクターです
https://gleam.run/

後で触れてみようと思っていたのに忘れていて遅くなりましたが 軽く使ってみたので 使い方とか感想です

Gleam

かなりシンプルな言語でドキュメントも短めでした
不満に感じる部分もほぼなくて良さそうに感じました

コンパイルする言語でコンパイル先が Erlang か JavaScript という少し特殊なものでした
ローカルで動かすなら Erlang でブラウザなら JavaScript という感じでの使い分けです
なのでローカルで動かす場合は Gleam に加えて Erlang も必要になります
信頼性のある Erlang のランタイムを使えることが魅力のように書かれてました
たしかに Erlang ってそういう評価をよく見ます

ただそういう作りということもあってか 標準ライブラリにはファイルシステムへのアクセスや日付の取得などが含まれていないようです
サードパーティのライブラリがあるのでそれを使う必要があります
ライブラリの中を見ると Erlang や JavaScript で実装されていてコンパイル対象によってそれぞれを使い分けてるようでした

JavaScript に変換するくらいなら最初から JavaScript を書くので 期待するのはローカルで動かすことなのですが Erlang は全然書けないんですよね
なので私の場合はできることは限られそうで そこが欠点かもしれません

CLI

最近の言語に多いと思いますが CLI ツールがリッチです
コンパイルする以外にもプロジェクトのテンプレート作成やパッケージマネージャやテストやフォーマッターなど一通り揃っています
珍しいところではドキュメント作成機能もありました
deno みたいな感じです

gleam new して作られたフォルダに移動して gleam run で動かせます
gleam new で作られるテンプレートにはテストコードもあるので gleam test でテスト実行もできます

構文

関数型言語というだけあって 他の関数型言語に近いと思います
Rust に結構似てると思います

簡単なコードだとこんな感じです

import gleam/io

pub fn main() {
io.debug(add(1, 2))
}

fn add(a: Int, b: Int) -> Int {
a + b
}

静的型付け言語なので型を書いてますが 引数も推論されるので省略可能です

import gleam/io

pub fn main() {
io.debug(add(1, 2))
}

// ↓これでもいい
fn add(a, b) {
a + b
}

ただ省略可能でも書くほうが推奨されるようです
外部公開なら書いておいたほうがいいかもですが 内部用で処理を分割するだけで型を書くのは面倒なので省略可能なのはいいところです
とはいえ やりすぎるとコードにミスがあったときに変な推論結果を元にエラーが出るので 原因がわかりづらくなります

ここまで省略可能だと静的型付け言語でも手間はあまりないですし 動的言語に近い感じで書けます

実行

gleam new でプロジェクトのテンプレートを作ります

[root@ddef652c5abe foo]# gleam new .
Your Gleam project foo has been successfully created.
The project can be compiled and tested by running these commands:

gleam test

[root@ddef652c5abe foo]# tree .
.
├── README.md
├── gleam.toml
├── src
│   └── foo.gleam
└── test
└── foo_test.gleam

3 directories, 4 files

gleam.toml が依存関係とかプロジェクトの情報を書くところです
Node.js の package.json や Rust の Cargo.toml や Python の pyproject.toml みたいなものです

name = "foo"
version = "1.0.0"

# Fill out these fields if you intend to generate HTML documentation or publish
# your project to the Hex package manager.
#
# description = ""
# licences = ["Apache-2.0"]
# repository = { type = "github", user = "username", repo = "project" }
# links = [{ title = "Website", href = "https://gleam.run" }]
#
# For a full reference of all the available options, you can have a look at
# https://gleam.run/writing-gleam/gleam-toml/.

[dependencies]
gleam_stdlib = "~> 0.34 or ~> 1.0"

[dev-dependencies]
gleeunit = "~> 1.0"

src/foo.gleam と test/foo_test.gleam はこんなのです

[root@ddef652c5abe foo]# cat src/foo.gleam
import gleam/io

pub fn main() {
io.println("Hello from foo!")
}

[root@ddef652c5abe foo]# cat test/foo_test.gleam
import gleeunit
import gleeunit/should

pub fn main() {
gleeunit.main()
}

// gleeunit test functions end in `_test`
pub fn hello_world_test() {
1
|> should.equal(1)
}

これを実行するときは gleam run で テストするなら gleam test です

[root@ddef652c5abe foo]# gleam run
Resolving versions
Downloading packages
Downloaded 2 packages in 0.07s
Compiling gleam_stdlib
Compiling gleeunit
Compiling foo
Compiled in 1.87s
Running foo.main
Hello from foo!

[root@ddef652c5abe foo]# gleam test
Compiled in 0.01s
Running foo_test.main
.
Finished in 0.021 seconds
1 tests, 0 failures

return がない

関数型言語でよく見かけるように 返り値は return 不要で最後の式の結果が使われます
そもそも return キーワードはないので 早期リターンはないです
ここは不便に思うことはあるかもですが ↓みたいなロジックで作ればいいので ないならないでそんなに困らないかなと思ってます
わかりやすくするようここは JavaScript のコードです

const fn = (value) => {
if (!value) {
return null
} else {
return _fn(value)
}
}

const _fn = (value) => {
// ...
}

イミュータブル

こっちも関数型言語でよく見かけるように値はイミュータブルです
mut や mutable と言ったキーワードはなくミュータブルにはできません

ただ こういうことはできます

import gleam/io.{debug}

pub fn main() {
let foo = 100
let foo = foo + 1
debug(foo)
}

同じ名前ですが 新しい foo を作る形なのでエラーになりません
新しい foo を作るときの右辺で古い foo を参照できます

文字列の扱い

Rust みたいな文字列の扱いづらさはないようで動的言語と近い感じで扱えるようです

ただし文字列の結合は少し変わっていて <> 演算子です
${} みたいな String Interpolation 機能は現在はないようです
ただ <> で十分か見てみるために今のところは対応しない みたいな感じで将来的に需要があれば追加されるのかもです
https://github.com/gleam-lang/gleam/issues/1473

string.concat 関数を使うこともできます

import gleam/io.{debug}
import gleam/string

pub fn main() {
let str1 = "foo" <> "bar"
let str2 = string.concat(["foo", "bar"])
debug(str1)
debug(str2)
}
"foobar"
"foobar"

関数の扱い

関数は変数のように扱えますし 式の中で直接書くこともできます

import gleam/io.{debug}

pub fn main() {
let return_one = fn () { 1 }
call(return_one)

call(fun1)
call(fn() { "text" })
}

fn fun1() {
10
}

fn call(fun) {
debug(fun())
}
1
10
"text"

関数を作った場所の外側のスコープを参照することもできます

import gleam/io.{debug}

pub fn main() {
let get = make_get()
debug(get())
}

fn make_get() {
let value = 100
fn() { value }
}
100

パイプオペレーター

オブジェクト指向言語じゃないのでオブジェクトのメソッドみたいなものはありません
基本は関数で操作します

import gleam/io.{debug}
import gleam/string
import gleam/list

pub fn main() {
debug(string.length("foo"))
debug(string.repeat("abc", 3))
debug(string.starts_with("xyz", "x"))
debug(string.join(["foo", "bar"], "/"))
debug(string.split("foo-bar", "-"))
debug(string.slice("abcdef", 2, 4))
debug(string.replace("1-2", "-", "/"))

debug(
string.join(
list.reverse(
list.map(
string.split("1-2-3-4-5", "-"),
fn(x) { "[" <> x <> "]" }
)
),
" "
)
)
}
3
"abcabcabc"
True
"foo/bar"
["foo", "bar"]
"cdef"
"1/2"
"[5] [4] [3] [2] [1]"

よくある形で内側から読まないといけなくて関数のネストがあるととても読みづらいです
ですがパイプオペレーターがあるのでメソッドチェーンのように上から下の流れで書けます

import gleam/io.{debug}
import gleam/string
import gleam/list

pub fn main() {
debug(
"1-2-3-4-5"
|> string.split("-")
|> list.map(fn(x) { "[" <> x <> "]" })
|> list.reverse
|> string.join(" ")
)
}
"[5] [4] [3] [2] [1]"

特殊な動き

パイプオペレーターの後は少し特殊な書き方ができるようです

import gleam/io.{debug}

pub fn main() {
debug(100 |> div(2))
debug(100 |> div(_, 2))
debug(100 |> div(200, _))
}

fn div(a, b) {
a / b
}
50
50
2

Gleam では関数はカリー化されていないので引数が足りないとエラーになります

import gleam/io.{debug}

pub fn main() {
let div10 = div(10)
debug(div10(2))
}

fn div(a, b) {
a / b
}
error: Incorrect arity
┌─ /opt/foo/src/foo.gleam:4:14

4 │ let div10 = div(10)
│ ^^^^^^^ Expected 2 arguments, got 1

ですがパイプオペレーターの右辺に書くものだと 一番最初の引数に左辺の値が入るので そこでのみ引数不足の関数呼び出しのコードを書けます

_ を使う方法は他でも使えます

import gleam/io.{debug}

pub fn main() {
let div2 = div(_, 2)
debug(div2(100))
}

fn div(a, b) {
a / b
}
50

_ 単体は特殊な意味を持つので 通常の変数に使えません
_ に代入は値を捨てるような動きになります

import gleam/io.{debug}

pub fn main() {
let _ = 1
f(_)
}

fn f(v) {
debug(v)
}

これは f(_) が関数呼び出しにならず関数を作るだけになって呼び出さないので出力はありません
他言語だと _ がユニット型と呼ばれているのもありますが Gleam だと Nil があってこれがユニット型になります

ブロック

ブロックが式として値を返します

import gleam/io.{debug}

pub fn main() {
let result = {
let tmp = 1 + 2
tmp * tmp
}

debug(result)
}
9

便利ですね
少し特殊なのは 式のグループ化にもこれを使います
掛け算より足し算を優先させたいとき 一般的には () を使いますが Gleam ではこれも {} になります

import gleam/io.{debug}

pub fn main() {
let value = { 1 + 2 } * 10
debug(value)
}
30

() は使えず {} を使うようエラーメッセージが出ます

import gleam/io.{debug}

pub fn main() {
let value = ( 1 + 2 ) * 10
debug(value)
}
error: Syntax error
┌─ /opt/foo/src/foo.gleam:4:14

4 │ let value = ( 1 + 2 ) * 10
│ ^ This parenthesis cannot be understood here

Hint: To group expressions in gleam use "{" and "}".

慣れないと他の言語の感覚で書けないので不便ですが けっこうアリな気はしています

分岐

if 文がなくて case によるパターンマッチングのみです
else if をしたいときにはちょっと不便です

switch を使える言語では true に対するマッチングで case に式を書いたりしますが Gleam の case はパターンの構文であって式ではないです
guard を使えば if に式を書けますが少し長くて見づらい感じです

単純に case をネストすることもできますが else if がいくつもあるとインデントが深くなっていきます

import gleam/io.{debug}

pub fn main() {
debug(use_nested_case(1))
debug(use_nested_case(-1))
debug(use_nested_case(0))

debug(use_guard(1))
debug(use_guard(-1))
debug(use_guard(0))
}

fn use_nested_case(v) {
case v == 0 {
True -> "zero"
False -> case v > 0 {
True -> "positive"
False -> "negative"
}
}
}

fn use_guard(v) {
case v {
_ if v > 0 -> "positive"
_ if v < 0 -> "negative"
_ -> "zero"
}
}
"positive"
"negative"
"zero"
"positive"
"negative"
"zero"

オブジェクト的なもの

リテラルで書ける JavaScript のオブジェクトのようなものはなくて事前に定義が必要なレコード型か タプルのリストからディクショナリ型を作るかになってデータをまとめるのは動的な言語よりは少し面倒です

レコード

import gleam/io.{debug}

type Type1 {
Value(num: Int, str: String)
}

pub fn main() {
let value1 = Value(1, "a")
let value2 = Value(num: 1, str: "a")

debug(value1.num)
debug(value2.str)
debug(value1 == value2)
}
1
"a"
True

Type1 のところの中には Value の 1 パターンだけですが ここに色々追加できます
それぞれがバリアントと呼ばれて 値を持つ場合はレコードと呼ばれます
レコードが 1 種類しかないときは 型名と名前を合わせてるのをよく見かけるので合わせるのが一般的かもしれません

type Type1 {
Type1(num: Int, str: String)
}

更新したい場合は よくあるスプレッド構文と更新内容の部分フィールドの組み合わせを使います
スプレッドは .. で 点が 2 つです

import gleam/io.{debug}

type Foo {
Foo(a: Int, b: Int, c: Int)
}

pub fn main() {
let foo = Foo(1, 2, 3)
debug(Foo(..foo, b: 10))
debug(Foo(..foo, a: 100, c: 300))
Nil
}
Foo(1, 10, 3)
Foo(100, 2, 300)

ディクショナリ

ディクショナリはリテラルで書けず リストから変換するか一つずつ要素を追加です

import gleam/io.{debug}
import gleam/dict
import gleam/result

pub fn main() {
let list1 = [#("foo", 1), #("bar", 2)]
let dict1 = dict.from_list(list1)

let dict2 = dict.new()
|> dict.insert("foo", 1)
|> dict.insert("bar", 2)

debug(dict1 |> dict.get("foo") |> result.unwrap(-1))
debug(dict2 |> dict.get("bar") |> result.unwrap(-1))
debug(dict1 == dict2)
debug(dict1 |> dict.get("baz") |> result.unwrap(-1))
debug(dict1)
}
1
2
True
-1
dict.from_list([#("bar", 2), #("foo", 1)])

ディクショナリの更新は dict2 みたいな感じで dict.insert を使います

リストのループ

gleam/list に map や filter を始めリスト処理の関数が揃っているのでリストは扱いやすい方ですが ときどき一つずつループするようなことをしたいです

JavaScript でいうこういう処理です

const items = [true, false, true]
let count = 0
for (const item of items) {
if (item) count++
}
console.log(count)

シンプルにしてるのでこれなら配列のメソッドで済むのですが こういう手続的に処理したいことがあります
そういう場合は 再帰関数にして count に当たるものを更新しながら常に次の関数に渡していって最後まできたらそれを返すと良いです

import gleam/io.{debug}

pub fn main() {
count_true([True, False, True]) |> debug
}

fn count_true(list) {
rec(list, 0)
}

fn rec(list, count) {
case list {
[first, ..rest] -> case first {
True -> rec(rest, count + 1)
False -> rec(rest, count)
}
[] -> count
}
}

Gleam には Tail call optimisation があるらしいので再帰でスタックがあふれる心配はしなくて良さそうです

ラップされた型

Result

最近は結構いろいろなところで見る気がする Result 型があります
中身は Ok と Error のレコードです
Ok と Error はインポートなしで使えます

import gleam/io.{debug}

pub fn main() {
debug(Ok(1))
debug(Error(2))
}
Ok(1)
Error(2)

これが例外の代わりで 結果が Ok または Error に包まれて帰ってきます
これらの型が Result になってます

Option

また null はなくて ユニット型に Nil がありますが Nullable ぽいものとして Option 型があります

pub type Option(a) {
Some(a)
None
}

値がある場合は Some で包んで 値がなければ None です

操作

これらは unwrap で中身を取り出せます
2 つめの引数がデフォルト値で Error の場合や None の場合に返す値を指定します

result の場合は unwrap_both でどちらの場合でも取り出して unwrap_error でエラーの場合のみ取り出すことができます

import gleam/io.{debug}
import gleam/result
import gleam/option

pub fn main() {
debug("--- result.unwrap ---")
Ok("OK") |> result.unwrap("default") |> debug
Error("ERROR") |> result.unwrap("default") |> debug

debug("--- result.unwrap_both ---")
Ok("OK") |> result.unwrap_both() |> debug
Error("ERROR") |> result.unwrap_both() |> debug

debug("--- result.unwrap_error ---")
Ok("OK") |> result.unwrap_error("default") |> debug
Error("ERROR") |> result.unwrap_error("default") |> debug

debug("--- option.unwrap ---")
option.Some("OK") |> option.unwrap("default") |> debug
option.None |> option.unwrap("default") |> debug
}
"--- result.unwrap ---"
"OK"
"default"
"--- result.unwrap_both ---"
"OK"
"ERROR"
"--- result.unwrap_error ---"
"default"
"ERROR"
"--- option.unwrap ---"
"OK"
"default"

同じ感じで map を使えば Result 型は Ok の場合のみ Option 型は Some の場合のみ値を更新できます
Error や None はそのまま維持されます
Result 型には try もあって map だと結果の値を返せば Ok で包まれるのに対して try は Ok か Error (Result 型) を返さないといけないです

import gleam/io.{debug}
import gleam/result

pub fn main() {
result.map(Ok(1), fn(x) { x + 1 }) |> debug
result.try(Ok(1), fn(x) { Ok(x + 1) }) |> debug
}
Ok(2)
Ok(2)

map で Result 型を返すと Result 型がネストされるので失敗する可能性があるなら try の方を使う感じです
名前の try にあってますね

use

use を使うと関数のネスト減らせます
関数に関数を渡す場合にこういう感じでどんどんネストが深くなります

import gleam/io.{debug}

pub fn main() {
getvalue1(fn(foo) {
getvalue2(fn(bar, baz) {
debug(foo)
debug(bar)
debug(baz)
})
})
}

fn getvalue1(callback) {
callback(1)
}

fn getvalue2(callback) {
callback(10, 20)
}
1
10
20

Node.js で懐かしのコールバック地獄というやつですね
これを回避する記法が use でこんな感じにできます

import gleam/io.{debug}

pub fn main() {
use foo <- getvalue1()
use bar, baz <- getvalue2()

debug(foo)
debug(bar)
debug(baz)
}

fn getvalue1(callback) {
callback(1)
}

fn getvalue2(callback) {
callback(10, 20)
}
1
10
20

他の関数型言語の let などで見かけるようなやつです

use 以降の式全体が関数に包まれるような形になるので use を使う関数の返り値に気をつける必要があります
use を使わない場合を見比べるとわかりやすいですが 最初の use の <- の右側の関数が返すものになります

これは try と組み合わせて使うことが多いようです
この例では直接 callback に数値を入れてますが try だと unwrap した内容を渡してくれます
Result 型の中身を使う場合どうしてもネストしてしまって unwrap するにしても早期リターンがないので場合分けのネストになってしまいます

import gleam/io.{debug}
import gleam/result

pub fn main() {
let value1 = getvalue1()
case result.is_ok(value1) {
True -> {
let value2 = getvalue2()
case result.is_ok(value2) {
True -> {
let value1 = result.unwrap(value1, 0)
let value2 = result.unwrap(value2, 0)
debug(value1 + value2)
Nil
}
False -> Nil
}
}
False -> Nil
}
}

fn getvalue1() {
Ok(1)
}

fn getvalue2() {
Ok(2)
}

なので Gleam だとこういう書き方になるようです

import gleam/io.{debug}
import gleam/result

pub fn main() {
use value1 <- result.try(getvalue1())
use value2 <- result.try(getvalue2())
debug(value1 + value2)
Ok(Nil)
}

fn getvalue1() {
Ok(1)
}

fn getvalue2() {
Ok(2)
}
3

try を使うと最後で Result を返さないといけなくなるので Ok(Nil) を置いてます
この場合は返り値を使わないので こういうケースは try じゃなくて map でも良さそうです

コメント

コメントは // を使います
ブロックコメントはなさそうです

ドキュメンテーションコメントは
モジュール → ////
関数や型 → ///

と分かれています

Java 系の Doc みたいな余計な装飾がなくていいですね

キーワード引数

少し変わった機能で キーワード引数に内部用の変数名と別の名前をつけることができます

JavaScript のこれみたいな感じです

const fn = ({ foo: bar }) => console.log(bar)
fn({ foo: 1 })
// 1

構文的には型も含めてこんな感じになります

import gleam/io.{debug}

pub fn main() {
func1(for_external: 1)
}

fn func1(for_external for_internal: Int) {
debug(for_internal)
}
1

型を省略すると

fn func1(a b, c d) {
//
}

になるので C 系の言語を見慣れていると a や c が型に見えてしまうんですよね
省略すると同じ名前になるのかというとそういうわけでもなく キーワード引数機能が無効になってしまいます
内部と同じ名前にしたいなら同じ名前を指定します

インポートのパス

相対パスのみ

インポートするときのパスですが 常に src フォルダからの相対パスでの指定になるようです
今のファイルからの相対パスは使えません

常に src からなら統一感があって 見た目は綺麗になりそうですが 同じフォルダのファイルのインポートで長いパスを書かないといけないのは面倒そうです
特にフォルダ単位で移動させたときに相対パスにしていれば修正がいらないこともありますが src からの相対パスは絶対パスみたいなものなので 確実に修正が必要になります

ライブラリとの競合

インストールしたパッケージと src 内のファイルでインポート方法に違いがありません
日付ライブラリの birl を入れて使う場合はこんな感じです

import birl
import gleam/io.{debug}

pub fn main() {
let now = birl.now()
debug(now |> birl.to_iso8601)
}
"2024-03-30T16:57:07.726Z"

src フォルダに birl.gleam モジュールがあると被ります
そうなると優先されるのは src フォルダの方です
ライブラリにアクセスできなくなります
ライブラリがあまり一般的な名前を使わなければ問題ないかもですが html とか json みたいな名前だと結構被りそうなんですよね

ちなみに gleam というモジュール名は予約語になっていて使えないようでした

おわり

思ってたより長くなりましたが ここに書いただけでドキュメントにある言語機能はほとんど含まれてると思います
あとは標準ライブラリの関数などでできることが増える感じです
それくらいにシンプルな言語のようです

個人的には今まで触れた関数型言語の中では一番とっつきやすくて良さそうに感じました
関数型言語になると変に複雑なものが多いですが Gleam はそういうのがほとんどないですし いろいろなところで使いやすさを感じました

普段使いするものというわけではないですが 今後の進化が気になる言語です