Open6

Gleam を触ってみる

yukiyuki

Gleam はプログラミング言語。コンパイラ自体はほぼ Rust でできているが、最終的な成果物は Erlang になって出てくる。書き味は Rust と Elixir を一部合体したみたいな感じがする。

https://gleam.run/

XXX 型みたいに分類するのは今のプログラミング言語は結構難しいけれど、副作用を発生させるとそこで一連のパイプラインが終了するみたいな副作用センシティブな設計に見えるので、関数型プログラミング言語の一種と捉えてよいのかなとは思った。ちなみにGleam自身はとくに公式でそういった点については言及しておらず、あくまでわたしの主観です。

yukiyuki

インストールなどはサイトを見てもらうとよさそう。必要なツールは一式そろった状態ではじめられるので、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>
yukiyuki

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.printlnString 型しか受け付けない。

なんかコンパイルエラーになって見に行ったら、io.printlnString を受け付けて Nil を返すみたいな実装だった。

関数の返り値の型としての Nil は実質他の言語で言う Unit とか void にあたりそう。Nil が返ってしまうとそこでパイプがそれ以上つながらないので、実質的に副作用を示す型として機能している気がした。

https://hexdocs.pm/gleam_stdlib/gleam/io.html#println

String 型以外を受け付けしたい場合は io.debug の方を用いる。こちらはシグネチャ的にジェネリックな形になっているので、どんな型であっても出力できる。a -> a みたいな変化ということは、出力後値を受け取って処理を続けられる??パイプ中心の設計になっている言語ならではという感じがする。

https://hexdocs.pm/gleam_stdlib/gleam/io.html#debug

yukiyuki

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 などは素直に表現できる。すると見通しがいいコードになるんじゃないかなあ、みたいな仮説を持っていた。この自分の仮説によくフィットしていい感じだなあと思う。

yukiyuki

これはあとで Discord で質問してみようかな。でも、環境の説明とか何入ってるかとかいろいろ説明しないといけなそうだから少々めんどくさそう…