Qiita Conference 2025

ymrl (@ymrl)

がんばらないアクセシビリティ: より幅広く価値を届けるために

3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【関数志向】F#の "let!" が何者かを説明する【コンピューテーション式】

Posted at

結論、これだけ

F#言語のコンピューテーション式とは何か?答えは簡単です。例えば以下のコンピューテーション式

Before
builder {
  let! x = expression
  return some_expression_using_x
}

これは以下の糖衣構文です。

After
builder.Bind(expression, fun x -> builder.Return some_expression_using_x)

ただこれだけです。しかしこれだけでは意義が分からないと思うので、この糖衣構文がどう役立つのかを説明します。

ロガーの例

例えば以下のように変数の内容をログに書き出したいとします。

let log p = printfn "expression is %A" p

let loggedWorkflow =
    let x = 42
    log x
    let y = x + 1
    log y
    //return
    y * 2

これを実行すると当然以下のような出力がされます。

expression is 42
expression is 43

これをもっとスマートに書きたいです。コンピューテーション式を活用すれば以下のように書くことが可能です。

let loggedWorkflow = logger {
  let! x = 42
  let! y = x + 1
  return y * 2
}

ログ出力したい部分をlogger {}でくくり、その中でlet!で束縛した内容がログに出力されるという仕組みです。とても読みやすいですね。こんな書き方ができるためには以下のようにloggerを定義すればよいです。

type LoggingBuilder() =
    let log p = printfn "expression is %A" p

    member this.Bind(x, f) =
        log x
        f x

    member this.Return(x) =
        x

let logger = new LoggingBuilder()

Bindの中でログ出力しています。このloggerを定義したうえで前述のコンピューテーション式を実行すると最初のコードと同じようにログ出力がされます。

expression is 42
expression is 43

処理の流れを追う

なぜこれで所望の動作になるのか考えます。まずは簡単な場合としてログ出力が1か所のみの以下のコードを考えます。

let loggedWorkflow = logger {
  let! x = 42
  return x + 1
}

これを実行すると以下の出力になります。

expression is 42

コンピューテーション式はただの糖衣構文なので上記は以下のように展開されます。

let loggedWorkflow = logger.Bind(
    42, fun x -> logger.Return x + 1
)

上記展開されたコードとloggerのコードから処理の流れを追ってみてください。logger.Bind42のログが出力された後 x + 1の部分の計算が行われ、確かに所望の動作になることが分かります。また当然ですが、展開後のコードと展開前のコンピューテーション式どちらを実行しても同じ動作、出力になります。

もともとの2つのlet!がある場合も全く同じように考えられます。糖衣構文を展開すると以下のようになります。

let loggedWorkflow = logger.Bind(
    42, fun x -> logger.Bind(
        x + 1, fun y -> logger.Return (y * 2)
    )
)

複数のlet!があるのでBindの中でBindが使われることになります。ただ糖衣構文の変換を2度適用しただけです。ちょっと複雑ですが処理の流れを追ってみると、確かにlet!で束縛した値がログに出力されるという挙動になります。

Optionの例

以下のようにOption型を伴う式でどこかでNoneになったらそこで処理を止めてNoneを返すような処理を考えます。

let map = Map [("a", Map [("z", 1)])]

let optionWorkflow =
  let x = map.TryFind "a"
  match x with
  | None -> None
  | Some xx ->
    let y = xx.TryFind "b"
    match y with
    | None -> None
    | Some yy -> yy + 1

入れ子のMapから順にキー"a""b"とたどってその値に1を足したものを返す、ただしキーが見つからなければNoneを返す、ということをしています。

これをコンピューテーション式を活用することで以下のように書けます。

let optionWorkflow = option {
  let! xx = map.TryFind "a"
  let! yy = xx.TryFind "b"
  return yy + 1
}

とても見やすくなりました。option {}でくくった中のlet!で束縛した値がNoneなら後続の処理を実行せず即座にNoneを返すという仕組みです。こんな書き方ができるためには以下のようにoptionを定義すればよいです。

type OptionBuilder() =

    member this.Bind(x: 'a option, f: 'a -> 'b option) =
        match x with
        | Some value -> f value
        | None -> None

    member this.Return(x) =
        Some x

let option = new OptionBuilder()

Bindの中で場合分けをして、NoneならNoneを返し、そうでなければその値を使ってfを実行します。このoptionを定義したうえで前述のコンピューテーション式を実行すると最初のコードと同じ効果が得られます。

処理の流れを追う

これもロガーの場合と全く同じです。この場合のコンピューテーション式は以下のように展開されます。

let optionWorkflow = option.Bind(
  map.TryFind "a", fun xx -> option.Bind(
    xx.TryFind "b", fun yy -> option.Return (yy + 1)
  )
)

処理の流れをひとつずつ追って、所望の動作になることを確かめてください。

そもそもこの糖衣構文はなにを意味しているのか?

let!が糖衣構文であり、それを活用することでいろいろ便利なことができることはわかりましたが、そもそもこの糖衣構文はどこから生まれて何を意味しているのでしょう?

builder.Bind(expression, fun x -> builder.Return some_expression_using_x)

結論を言うと、これは「手続き型言語における変数(定数)定義を関数型的に表現したもの」です。変数定義とはF#でいうlet a = 1の様な構文ですね。実はこのletのような変数定義は厳格な関数型の思想では許されないです。じゃあこの変数定義と同じことを関数型でどう行うのかという問いに対する答えが上の書き方になります。

と言っても意味不明だと思うので説明します。まず関数型言語ではすべてを入力と出力がある関数で考えます。手続き型言語における変数定義というものは、まず変数を定義し、その後その変数を定義以降のどこかで使います。この「定義以降のどこか」で変数を使っている部分は変数の値に依存する処理であり、言い換えれば変数の値を入力とする関数ととらえられます。

具体例を示すとletを使って変数qを作っている以下のコード

let divmod (x, y) =
    let q = x / y
    (q, x - q * y)

これは厳格な関数指向に沿った、変数を使わない以下のコードに変換できます。

let divmod (x, y) =
    (fun q -> (q, x - q * y)) (x / y)
    // または以下のように書いた方が分かりやすいかも
    // (x / y) |> (fun q -> (q, x - q * y))

変数を使っている部分はfun q -> (q, x - q * y)のように定義した変数を入力とする関数になっていることが分かります。

つまりこれはletという変数定義構文を、関数指向の考え方で以下の同値のコードに解釈しなおすことができるという事です。

let Bind (v, f) = f v

let divmod (x, y) =
    Bind (x / y, (fun q -> (q, x - q * y)))

これはlet!の糖衣構文がやっているのと同じ変換です。

参考文献

以下の記事が無茶苦茶分かりやすかった

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up

Qiita Conference 2025 will be held!: 4/23(wed) - 4/25(Fri)

Qiita Conference is the largest tech conference in Qiita!

Keynote Speaker

ymrl、Masanobu Naruse, Takeshi Kano, Junichi Ito, uhyo, Hiroshi Tokumaru, MinoDriven, Minorun, Hiroyuki Sakuraba, tenntenn, drken, konifar

View event details
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?