Gleam を触ってみる
Gleam はプログラミング言語。コンパイラ自体はほぼ Rust でできているが、最終的な成果物は Erlang になって出てくる。書き味は Rust と Elixir を一部合体したみたいな感じがする。
XXX 型みたいに分類するのは今のプログラミング言語は結構難しいけれど、副作用を発生させるとそこで一連のパイプラインが終了するみたいな副作用センシティブな設計に見えるので、関数型プログラミング言語の一種と捉えてよいのかなとは思った。ちなみにGleam自身はとくに公式でそういった点については言及しておらず、あくまでわたしの主観です。
インストールなどはサイトを見てもらうとよさそう。必要なツールは一式そろった状態ではじめられるので、Gleam をインストールしておいた状態で開始できる。
プロジェクトをはじめる〜Hello, worldくらいまで
gleam new [プロジェクト名]
すると、git や GitHub Actions の設定まで含めたプロジェクトが立ち上がる。
Hello, world
こんな感じのコードが生成されているので…
import gleam/io
pub fn main() {
io.println("Hello from first_gleam!")
}
下記のように run コマンドを入れるとプログラムを実行できる。
❯ gleam run
Compiling first_gleam
Compiled in 0.50s
Running first_gleam.main
Hello from first_gleam!
インタプリタも立ち上がる。
❯ gleam shell
Compiling first_gleam
Compiled in 0.43s
Running Erlang shell
Erlang/OTP 24 [erts-12.2] [source] [64-bit] [smp:16:16] [ds:16:16:10] [async-threads:1] [jit] [dtrace]
Eshell V12.2 (abort with ^G)
1>
fizzbuzz を書いてみた
FizzBuzz を書いてみた。たぶん合っていると思う…
import gleam/io
import gleam/list
import gleam/int
pub fn do_fizzbuzz(n: Int) -> String {
case n % 5, n % 3 {
0, 0 -> "FizzBuzz"
0, _ -> "Fizz"
_, 0 -> "Buzz"
_, _ -> int.to_string(n)
}
}
pub fn main() {
list.range(1, 101)
|> list.map(do_fizzbuzz)
|> io.debug
}
つまづきポイントがいくつかあったので、簡単にメモしておく。
if 文/式はないみたい
ない。代わりにパターンマッチングをできる case
でがんばる。
なんかガードが使えない
パターンマッチングでガードを使って楽に書けるのかと思ったけれど、ガード節に入れられるものに制限があるみたい?
下記のコードは一見すると通りそうだけど、コンパイルエラー。
pub fn main() {
let fizzbuzz = list.range(1, 101) |> list.map(fn(n) {
case n {
n if n % 5 == 0 && n % 3 == 0 -> "FizzBuzz"
n if n % 5 -> "Fizz"
n if n % 3 -> "Buzz"
_ -> int.to_string(n)
}
})
io.debug(fizzbuzz)
}
if
のパースはうまく行っていそうで、そのあとがダメそう。ちなみにドキュメントを見ると、ガード節自体は存在するので、受け付けできる演算に限りがあるのかなあと思った。ドキュメントによれば「返す結果が True / False になる演算なら受付可能」なように見えるけれど。%
自体も演算として定義されているみたいだった。
error: Syntax error
┌─ src/fizzbuzz.gleam:32:14
│
32 │ n if n % 5 == 0 && n % 3 == 0 -> "FizzBuzz"
│ ^ I was not expecting this.
Expected one of: "->"
ただ、ガードを使わなければいけるはずと思い次のようなコードにした。
pub fn do_fizzbuzz(n: Int) -> String {
case n % 5, n % 3 {
0, 0 -> "FizzBuzz"
0, _ -> "Fizz"
_, 0 -> "Buzz"
_, _ -> int.to_string(n)
}
}
下記は「多値(multiple values)」。多値はパターンマッチングできる。
case n % 5, n % 3
結果に応じて Fizz なり Buzz なりを出力する。
case n % 5, n % 3 {
0, 0 -> "FizzBuzz"
0, _ -> "Fizz"
_, 0 -> "Buzz"
_, _ -> int.to_string(n)
}
|>
Gleam はパイプを中心にしてプログラミングするスタイルみたい。他のサンプルコードを見ていても、結構パイプを多用していた。
a |> b
みたいに書くと、a の結果を受け取って b の引数として渡す、みたいなことをできる?あんまりパイプがわかっていないかも。ちなみに 1 |> io.debug
みたいにも書けるのでモナドじゃなきゃいけないとかそういう制約はないみたい。
list.range(1, 101)
|> list.map(do_fizzbuzz)
io.println
は String
型しか受け付けない。
なんかコンパイルエラーになって見に行ったら、io.println
は String
を受け付けて Nil
を返すみたいな実装だった。
関数の返り値の型としての Nil
は実質他の言語で言う Unit
とか void
にあたりそう。Nil
が返ってしまうとそこでパイプがそれ以上つながらないので、実質的に副作用を示す型として機能している気がした。
String 型以外を受け付けしたい場合は io.debug
の方を用いる。こちらはシグネチャ的にジェネリックな形になっているので、どんな型であっても出力できる。a -> a
みたいな変化ということは、出力後値を受け取って処理を続けられる??パイプ中心の設計になっている言語ならではという感じがする。
HTTP サーバーを用意してみる
結構多そうなユースケースとしては Rust っぽい文法で Erlang の恩恵を受けられるみたいなところだと思うので、HTTP サーバーかなと思った。一応用意されているのでそれを使ってみる。
さっそくだけど、ちょっとエラーメッセージがわかりにくい。
gleam add http
この結果得られるメッセージは下記のとおりだった。これだと、そういうライブラリが見つからなかったのか、それともバージョン指定が必要なのかよくわからない。結果前者の「そういう名前のライブラリはない」だったけど。
❯ gleam add http
Resolving versions
error: Dependency resolution failed
An error occured while determining what dependency packages and versions
should be downloaded.
Decision making failed
パッケージ名は「gleam_http」が正解だった!
❯ gleam add gleam_http
Resolving versions
Downloading packages
Downloaded 3 packages in 1.10s
Added gleam_http v3.0.0
一旦コードを書いてみる。
import gleam/io
import gleam/bit_builder.{BitBuilder}
import gleam/http/elli
import gleam/http/request.{Request}
import gleam/http/response.{Response}
fn service(_req: Request(BitString)) -> Response(BitBuilder) {
let body = bit_builder.from_string("Hello, my server!")
response.new(200)
|> response.set_body(body)
}
pub fn main() {
elli.become(service, on_port: 3000)
}
コンパイルしてみてエラーメッセージを読み解いてみたところ、2つほど依存するライブラリが足りないみたい。
- gleam_elli
- gleam_otp
それぞれを add してもう一度コンパイルしてみる。そしてなんだこれは。
❯ gleam build
Compiling gleam_otp
build/dev/erlang/gleam_otp/gleam_otp_external.erl:17:14: can't find include lib "gleam_otp/include/gleam@otp@process_Sender.hrl"
% 17| -include_lib("gleam_otp/include/gleam@otp@process_Sender.hrl").
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:18:14: can't find include lib "gleam_otp/include/gleam@otp@process_Exit.hrl"
% 18| -include_lib("gleam_otp/include/gleam@otp@process_Exit.hrl").
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:19:14: can't find include lib "gleam_otp/include/gleam@otp@process_PortDown.hrl"
% 19| -include_lib("gleam_otp/include/gleam@otp@process_PortDown.hrl").
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:20:14: can't find include lib "gleam_otp/include/gleam@otp@process_ProcessDown.hrl"
% 20| -include_lib("gleam_otp/include/gleam@otp@process_ProcessDown.hrl").
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:21:14: can't find include lib "gleam_otp/include/gleam@otp@process_StatusInfo.hrl"
% 21| -include_lib("gleam_otp/include/gleam@otp@process_StatusInfo.hrl").
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:122:54: record process_down undefined
% 122| transform_msg(Receiving, {process, Ref}, #process_down{pid = Pid, reason = Reason});
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:125:51: record port_down undefined
% 125| transform_msg(Receiving, {port, Ref}, #port_down{port = Port, reason = Reason});
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:128:44: record exit undefined
% 128| transform_msg(Receiving, exit, #exit{pid = Pid, reason = Reason});
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:197:14: record sender undefined
% 197| Sender = #sender{pid = Pid, prepare = {some, Prepare}},
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:210:5: record status_info undefined
% 210| #status_info{mode = Mode, parent = Parent, debug_state = Debug,
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:213:16: variable 'Mode' is unbound
% 213| get(), Mode, Parent, Debug,
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:213:22: variable 'Parent' is unbound
% 213| get(), Mode, Parent, Debug,
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:213:30: variable 'Debug' is unbound
% 213| get(), Mode, Parent, Debug,
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:215:29: variable 'Mode' is unbound
% 215| {data, [{'Status', Mode}, {'Parent', Parent}, {'State', State}]}]
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:215:47: variable 'Parent' is unbound
% 215| {data, [{'Status', Mode}, {'Parent', Parent}, {'State', State}]}]
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:215:66: variable 'State' is unbound
% 215| {data, [{'Status', Mode}, {'Parent', Parent}, {'State', State}]}]
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:217:31: variable 'Mod' is unbound
% 217| {status, self(), {module, Mod}, Data}.
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:121:32: Warning: variable 'Pid' is unused
% 121| {'DOWN', Ref, process, Pid, Reason} when is_map_key({process, Ref}, Receiving) ->
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:121:37: Warning: variable 'Reason' is unused
% 121| {'DOWN', Ref, process, Pid, Reason} when is_map_key({process, Ref}, Receiving) ->
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:124:29: Warning: variable 'Port' is unused
% 124| {'DOWN', Ref, port, Port, Reason} when is_map_key({port, Ref}, Receiving) ->
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:124:35: Warning: variable 'Reason' is unused
% 124| {'DOWN', Ref, port, Port, Reason} when is_map_key({port, Ref}, Receiving) ->
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:127:18: Warning: variable 'Pid' is unused
% 127| {'EXIT', Pid, Reason} when is_map_key(exit, Receiving) ->
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:127:23: Warning: variable 'Reason' is unused
% 127| {'EXIT', Pid, Reason} when is_map_key(exit, Receiving) ->
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:195:13: Warning: variable 'Pid' is unused
% 195| system_msg({Pid, Ref}, Tag) ->
% | ^
build/dev/erlang/gleam_otp/gleam_otp_external.erl:196:5: Warning: variable 'Prepare' is unused
% 196| Prepare = fun(X) -> system_reply(Tag, Ref, X) end,
% | ^
Erlang 側のエラーっぽいけど、Erlang が全然わからないので詰んだ。
コンパイルは通らなかったけど、パイプを使った Web サーバーは相当書き味がよさそうに思っている。Scala のときも思ったけれど、HTTP サーバーというのは究極 Request を受け取ってデータを取り出し→(何かしらの処理)→終わったら Response の形に変えて変換して返す、みたいな Request → Response の型遷移でしかなく、ここをパイプや for-yield などは素直に表現できる。すると見通しがいいコードになるんじゃないかなあ、みたいな仮説を持っていた。この自分の仮説によくフィットしていい感じだなあと思う。
これはあとで Discord で質問してみようかな。でも、環境の説明とか何入ってるかとかいろいろ説明しないといけなそうだから少々めんどくさそう…
実験して遊んでいるリポジトリ