🦊

Gleamのuseについて

2024/05/15に公開

今回はGleamのuseについて解説します。サンプルプログラムはComamoca/sandboxにて公開しています。

useとは、端的に言うと高階関数をネストせずフラットに記述できる糖衣構文です。

主にコールバックを受け取るresult.tryなどの関数や、WebフレームワークWispのミドルウェアなどで使われています。

wispのミドルウェアでの使用例
import wisp.{type Request, type Response}

pub fn handle_request(request: Request) -> Response {
  use <- wisp.log_request(request)
  use <- wisp.serve_static(request, under: "/static", from: "/public")
  wisp.ok()
}

構文

まずはuseの構文を解説します。

書式

useは次のように書きます。

  • 高階関数がfn (value) -> valueのように、引数を受け取る形の場合
use value <- func()
// `value`を使った処理を書く
io.println(value)
  • 高階関数がfn () -> valueのように、引数を受け取らない形の場合
use <- func()
io.println("Hello")

これは以下のコードと実質的に等価です。

  • 高階関数がfn (value) -> valueのように、引数を受け取る形の場合
func(fn (value) {
  // `value`を使った処理を書く
  io.println(value)
})
  • 高階関数がfn () -> valueのように、引数を受け取らない形の場合
func(fn () {
  io.println("Hello")
})

サンプルプログラム

以下にuseを使った場合とそうでない場合のサンプルプログラムを示します。

useで引数を受け取る場合

数値が偶数か判定しResultを返すeven関数を使い、even関数の戻り値に依存する処理を実行するサンプルです。

import gleam/io
import gleam/result

pub fn main() {
  io.debug(with_use())     // Ok(4)
  io.debug(without_use())  // Ok(4)
}

fn even(n: Int) -> Result(Int, String) {
  case n % 2 {
    0 -> Ok(n)
    _ -> Error("Not even.")
  }
}

pub fn without_use() {
  result.try(even(2), fn(n) { Ok(n * 2) })
}

pub fn with_use() {
  use n <- result.try(even(2))
  Ok(n * 2)
}

useで引数を受け取らない場合

文字列を返す関数middle_1middle_2を連鎖的に呼び出すサンプルです。
Gleamのwebフレームワークwispのmiddlewareを参考に書きました。

pub fn with_use_novalue() {
  use <- middle_1()
  use <- middle_2()
  "hi"
}

pub fn without_use_novalue() {
  middle_2(fn() { middle_1(fn() { "hi" }) })
}

fn middle_1(next: fn() -> String) {
  next()
}

fn middle_2(next: fn() -> String) {
  next()
}

useの境界

useを使っていると以下のようなuseのスコープの終了を明示的に示したいケースに遭遇することがあります。しかしこのコードはエラーが発生します。

なぜなら"return"の箇所までがuseのスコープだと解釈されるからです。
この場合、result.tryfn(a) -> Result(c, b)ではなくfn(a) -> Stringが与えられエラーになります。

fn fetch_content() -> String {
  // httpc.sendはResult(Response(String), Dynamic)を返すので、一旦Resultを剥したい
  use resp <- result.try(httpc.send(req))
  io.debug(resp)
  
  Ok(Nil)
  
  // Stringを返す関数なので最後に文字列を置きたい
  "return"
}

この場合はブロック構文を使ってuseをスコープ内で実行する必要があります。

fn fetch_content() -> String {
  let body = {
    use resp <- result.try(httpc.send(req))

    Ok(resp.body)
  }

  io.debug(body)
  
  // 関数の定義通りStringを返せる
  "return" 
}

なおこのブロック構文は戻り値を受け取らなくてもかまいません
上記のコードのlet body = ...の箇所はこのようにも書けます。

{
  use resp <- result.try(httpc.send(req))
  io.debug(resp.body)
  Ok(Nil)
}

実用的なサンプルコード

sandbox/ex_gleam_httpcにてGleamのHTTPクライアントhttpcを使ったサンプルコードを公開しています。どのように使うのかイメージを掴めていただければ幸いです。

以下は使われている箇所の抜粋です。

fetch.gleam24行目から27行目
https://github.com/Comamoca/sandbox/blob/15563a1e6990a61643624d9b9880d9cd3e3893f9/ex_gleam_httpc/src/fetch.gleam#L24-L27

ex_geam_httpc.geam23行目から32行目
https://github.com/Comamoca/sandbox/blob/15563a1e6990a61643624d9b9880d9cd3e3893f9/ex_gleam_httpc/src/ex_gleam_httpc.gleam#L23-L32

useはいつ使うべきでいつ使うべきではないのか

以下の条件に当てはまるのなら、使うことを検討すべきだと考えます。

  • コールバックが2つ以上ネストしている
  • 高階関数を複数連続で実行させたい

コールバックが2つ以上ネストしている

冒頭でも述べたとおり、useはresult.tryなどのコールバックを受け取る関数などで威力を発揮します。
しかし、useは例外処理や非同期処理を内包する構文となっているため、[2]それ以外でも使うことが可能です。

GleamにはElixir Taskのようにプロセス生成を手軽にできるgleam/otp/taskというモジュールがあります。

そのgleam/otp/taskにあるtask.async関数の仕様は以下のようになっており、useを用いて簡潔に処理を書けます。

pub fn async(work: fn() -> a) -> Task(a)

以下は模式的なサンプルコードです。

import gleam/otp/task

pub fn main() {
  fetch_proc()
  |> task.await_forever() // 終了するまで無限に待機
  |> io.println
}

fn fetch_proc() -> Task(String){
  use <- task.async
  // 重い処理、時間のかかる処理

  "result" // 戻り値
}

高階関数を再帰的に複数連続で実行させたい

fn (fun: fn () -> value)のような仕様の関数を連鎖させて実行させたいなどの場合にはuseの使用を考えるべきです。

import gleam/int
import gleam/io

pub fn main() {
  use <- foo()
  use <- mul2()
  "4"
}

fn foo(fun: fn() -> String) {
  let return = fun()
  io.debug("This is foo")
}

fn mul2(fun: fn() -> String) {
  let return = fun()

  case int.parse(fun()) {
    Ok(n) -> int.to_string(n * 2)
    Error(_) -> "not number"
  }
  |> io.debug

  return
}

関数を遅延実行させる

Goのdeferのような、処理が終わった時に特定の処理を実行させたい際もuseが使えます。

import gleam/io

pub fn main() {
  use <- defer(fn() { io.println("Goodbye") })
  io.println("Hello!")
}

fn defer(cleanup, body) {
  body()
  cleanup()
}

逆にこれらのケースに合わない場合は使うのを避けた方が良いでしょう。

まとめ

今回はGleamのuseについて解説しました。
useを使うと処理をスッキリと書けるので、適度に使って楽しくGleamを書いていきましょう!

脚注
  1. ダッシュボードから右上のdevboxをクリックし、検索欄にGleamと入力すれば出てきます。 ↩︎

  2. https://github.com/gleam-lang/gleam/discussions/2051#discussioncomment-5213412 ↩︎

GitHubで編集を提案

Discussion