読者です 読者をやめる 読者になる 読者になる

コンピュテーション式の Tips

この記事は F# Advent Calendar 2014 - connpass の初日の記事です。

12月になってしまいましたね、進捗どうですか?私は駄目です。

タイトルは"コンピュテーション式の Tips"となっていますが、中身は Basis.Core"Persimmon 内で使われているコンピュテーション式の実装方法を勝手に紹介するというだけです。

はじめに

この記事は 2014/12/01 JST 現在書きかけです。 よって、後々更新したときのために gist で diff を残しておきます。

FsAdvent.2014.md

モナド

  • 目的: Haskellのdo構文をエミュレート
  • 実装に使うもの: monad laws, Return, Bind, ReturnFrom (, Zero)

コンピュテーション式で一番よく知られている(そして誤解するかもしれない)パターン。

do 構文のエミュレーションに必要な最低限のメソッドのみ実装する。

// example
type OptionBuilder() =
  member __.Bind(x, f) = Option.bind f x
  member __.Return(x) = Some x
  member __.ReturnFrom(x: _ option) = x

let option = OptionBuilder()

let l = [10..99]

option {
  let! a = List.tryFind (fun x -> x % 2 = 0) l
  let! b = List.tryFind (fun x -> x % a = 9) l
  return (a, b)
}

フロー制御

  • 目的: 使うキーワードによって後続の計算を分岐させたい
  • 実装に使うもの: State, CPS, (try-with)

これは既に id:bleis-tift さんが詳しく解説しているので、紹介にとどめておきます。

more information: yield and return (poor English ver)

例外

  • 目的: 最小限のBuilderで例外をハンドリングする
  • 実装に使うもの: try-with, custom operator

参考: https://github.com/persimmon-projects/Persimmon/blob/87a83d55602268f43a6d31e04ac47dafa2bd8de4/Persimmon/ComputationExpressions.fs#L74

拡張メソッド

  • 目的: 既存のコンピュテーション式を拡張したい
  • 実装に使うもの: 拡張メソッド
//examle 1
open Basis.Core

type Option.OptionBuilder with
  member this.Source(x) = Result.toOption x
  member this.Source(x: _ option) = x
    
let res = option { return! Success 10 } // Some 10
//example 2
open System.Threading.Tasks

type AsyncBuilder with
  member __.Source(x: Task<_>) = Async.AwaitTask(x)
  member __.Source(xs) = Async.Parallel(xs)

let task =
  async {
    return 1
  }
  |> Async.StartAsTask

async {
  let! x = task
  return printfn "%d" (x + 1)
}
|> Async.Start

async {
  let! xs = [0..10] |> List.map (fun x -> async { return x })
  return printfn "%A" xs
}
|> Async.Start

型によるDSL制御

  • 目的: キーワードの呼び出し順序を固定したい
  • 実装に使うもの: 判別共用体(DU), custom operator, Sourceメソッド

unit

  • 目的: unitとそれ以外の型で挙動を変えたい
  • 実装に使うもの: 判別共用体, Sourceメソッド

例: https://github.com/persimmon-projects/Persimmon/blob/87a83d55602268f43a6d31e04ac47dafa2bd8de4/Persimmon/ComputationExpressions.fs#L19

Quotation Evaluation

query ビルダーが公式サンプルである。

カスタムオペレーター

TODO: write

why reverse?

カスタムオペレーターに関するおまけ。

type SampleBuilder() =
  member __.Yield(()) = Seq.empty
  member __.Yield(x) = x
  member __.For (state, _) = state
  [<CustomOperation("case", AllowIntoPattern=true)>]
  member inline __.Case(source, case) = seq { yield! source; yield case }
  [<CustomOperation("run")>]
  member __.SideEffect(source: _ seq, [<ProjectionParameter>]f: _ * _ -> unit) =
    source |> Seq.iter f

let sample = SampleBuilder()

(*
expected output:
1, 2
3, 4

but was:
2, 1
4, 3
*)
sample {
  case (1, 2)
  case (3, 4) into (x, y)
  run (printfn "%d, %d" x y)
}

なぜかタプルが逆順で出力されるのですよねー…仕様なのかバグなのか。

おわりに

TODO: write

余談

最初は FSharp.KarmaHigher や (未完成の) FreeF の解説を書こうかと思っていたけど、テンションがあがらなかったので断念。 それもこれも FreeF が未完成なのが悪い。 Please give me "higher kinded types"!