Skip to content

Instantly share code, notes, and snippets.

@pocketberserker
Created September 13, 2015 01:59
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pocketberserker/11df017ae5f18befe2da to your computer and use it in GitHub Desktop.
Save pocketberserker/11df017ae5f18befe2da to your computer and use it in GitHub Desktop.
関数プログラミング交流会 - モナドとかよくわからないからコンピュテーション式でガン無視してみた

モナドとかよくわからないからコンピュテーション式でガン無視してみた

CC BY-SA 4.0

自己紹介

icon

  • なかやん・ゆーき / ぺんぎん / もみあげ
  • @pocketberserker / id:pocketberserker
  • Microsoft MVP for F# .NET (2015/04/01~ 2016/03/31)
  • モナドわかりません

F#とは

  • .NET向け関数プログラミング言語
  • 7月に 4.0 がリリースされた
    • ひと安心?
    • VS2015ではオプショナルインストールだけどな!(これはC++もなのでVisual Studioの方針の問題)
  • 現実をみて色々と割り切ってる(例: 非変のみ)
  • 人によっては中途半端に感じるかもしれない
  • でもそこが可愛いのですよ
  • 日本での利用率はそんなに高くない

F#のコンピュテーション式

  • カスタマイズポイントを提供した
  • F# に似た文法を記述すると変換規則に従って式に変換してくれる
    • 変換規則にでてくるメソッドを定義する必要がある
    • 各メソッドは定義時は何の関連性もない
    • つまりメソッド定義ではコンパイルエラーにならなくても変換側に型があわない、などが発生する
  • F# 3.0で
  • MSDNのコンピュテーション式の説明はF# 2.0のまま…
  • おすすめの解説記事: 詳説コンピュテーション式
type OptionBuilder() =
  member __.Bind(x, f) = Option.bind f x
  member __.Return(x) = Some x
  member __.ReturnFrom(x) = x

// Some(x + y) もしくは None
option {
  let! x = Some 1
  let! y = findHoge
  return x + y
}

コンピュテーション式は~ではない

  • 単なるdo構文の代わりではない
    • そういう使い方もできる、という話
  • 単なるfor式の代わりではない
    • そういう使い方も(ry
  • ○○専用構文ではない
    • async/await特化とかではない

コンピュテーション式についての考察その1

http://www.slideshare.net/bleistift/yieldreturn

  • yieldreturn について考察した資料
  • StateやContを使ってコンピュテーション式を実装すると色々制御できるよという話ものっている
  • 参考: https://github.com/BasisLib/Basis.Core

コンピュテーション式についての考察その2

https://github.com/persimmon-projects/Persimmon におけるいくつかのコンピュテーション式について

  • 今日の本題
  • Persimmon: コンピュテーション式を利用したユニットテスティングフレームワーク
    • 合成可能なテストケース
    • soft assertion
    • 最近Scalaにそれっぽく移植した
      • https://github.com/pocketberserker/dog
      • TestCase は Kleisli (なのでモナド則を満たすだろう)
      • アサーションはright biased Either (なのでモナド則を満たすだろう)

ここで一区切り

  • 一区切り的な意味で唐突に高階ことりちゃん
  • http://tkotori.github.io/
  • ことりちゃんは癒し
  • ことりちゃんの画像はCC BY-SA 3.0の範囲で自由に利用できる

ちゅんちゅん

モナドをガン無視する

  • "モナドは単なる自己関手の圏におけるモノイド対象だよ。何か問題でも?"
  • 問題ある
  • F# ではモナド則を満たすかどうか証明する手段はない
    • もちろん FsCheck(QuickCheck的なやつ)で「たぶん満たすだろう」と言うことはできる
  • 独自のモナドを考えるのはつらい
    • ということにしておく

"関数プログラミング実践入門" p.263 より

ただ、ビルダーが提供されコンピュテーション式で使えるからと言って、モナドになっているとは限らないことには注意が必要です。
  • しかし何も書かずにコンピュテーション式を実装してるとモナドなのかと勘違いされかねない
    • ある種の弊害か…
    • モナドでない場合はドキュメントにそのことを記載したほうが安全

なら無視しよう

どんな感じに無視するの?

  • srcを用いたオーバーロードもどき
  • カスタムオペレータの悪用
  • 例外ハンドリング
  • Quoteによるコンピュテーション式の解析
  • (型拡張、継承を用いた構文捻じ曲げ) 今回の対象外

srcとSourceメソッド

T(let! p = e in ce, V, C, q) =  T(ce, V Å var(p), lv.C(b.Bind(src(e),fun p -> v), q)
T(yield! e, V, C, q) = C(b.YieldFrom(src(e)))
T(return! e, V, C, q) = C(b.ReturnFrom(src(e)))
T(use! p = e in ce, V, C, q) = C(b.Bind(src(e), fun p -> b.Using(p, fun p -> {| ce |}0))
T(for p1 in e1 do joinOp p2 in e2 onWord (e3 eop e4) ce, V, C, q) =
    Assert(q); T(for pat(V) in b.Join(src(e1), src(e2), lp1.e3, lp2.e4, 
    lp1. lp2.(p1,p2)) do ce, V , C, q)
T(for p1 in e1 do groupJoinOp p2 in e2 onWord (e3 eop e4) into p3 ce, V, C, q) = 
    Assert(q); T(for pat(V) in b.GroupJoin(src(e1), 
    src(e2), lp1.e3, lp2.e4, lp1. lp3.(p1,p3)) do ce, V , C, q)
T(for x in e do ce, V, C, q) = T(ce, V Å {x}, lv.C(b.For(src(e), fun x -> v)), q)
T(do! e;, V, C, q) = T(let! () = src(e) in b.Return(), V, C, q)
  • ビルダーにSourceメソッドがあればsrcがSourceに変換される
  • Bindの第一引数の型にあうように変換できるSourceを定義しておけば暗黙的変換(もしくはオーバーロード)を再現できる
// ... -> Either<Exeption, int>
let throwableF ... 

type OptionBuilder with
  member __.Source(x: Either<_, _>) =
    match x with
    | Left(_) -> None
    | Right(x) => Some x

option {
  let! x = throwableF // Sourceでoptionに変換される
  let! y = Some 2
  return x + y
}

PersimmonとSourceメソッド

BindingValueという判別共用体を用意する

type BindingValue<'T> =
  | UnitAssertionResult of AssertionResult<'T (* unit *)>
  | NonUnitAssertionResult of AssertionResult<'T>
  | UnitTestCase of TestCase<'T (* unit *)>
  | NonUnitTestCase of TestCase<'T>

この型はSourceのみで利用される

  • Bind内ではBindingValueをパターンマッチ
  • UnitAssertionResult, UnitTestCaseではテスト続行、それ以外はテスト失敗としてテストを合成
    • unitはUnchecked.defaultof<'T>で値が取得できるのでできる芸当
    • Bind内では'Tで型が固定されているためコンパイル時に型チェックが可能

カスタムオペレータ

  • コンピュテーション式内でユーザ独自のキーワードを定義できるようになる
  • 制約が色々あったりして使いどころが難しい
  • F# のクエリ式はカスタムオペレータを使って実装されている

Persimmonとカスタムオペレータ

パラメタライズドテストを表現するために利用

let `parameterize test` = parameterize {
  case (1, 1)
  case (1, 2)
  run (fun (x, y) -> test "case parameterize test" {
    do! assertEquals x y
  })
}

let `パラメタライズテストそのに` =
  let innerTest (x, y) = test {
    do! assertEquals x y
  }
  parameterize {
    source [
      (1, 1)
      (1, 2)
    ]
    run innerTest
  }

カスタムオペレータの悪用?

  • 各メソッドは定義時は何の関連性もない
  • メソッドなのでもちろん引数や戻り値にジェネリックが使える
  • タプルを使うことできちんと型チェックできる
    • NUnitとは違うのです

例外ハンドリング

  • 期待する例外が投げられたかどうかテストしたい
  • しかしTestコンピュテーションビルダーの拡張は避けたい
    • 複雑になるから
  • 解決策: 別のコンピュテーション式で例外をキャッチする
type TrapBuilder () =
  member __.Zero () = ()
  member __.Yield(()) = Seq.empty
  [<CustomOperation("it")>]
  member __.It(_, f) = f
  member __.Delay(f: unit -> _) = f
  member __.Run(f) =
    try
      f () |> ignore
      fail "Expect thrown exn but not"
    with
      e -> pass e
      
let trap = TrapBuilder()

このコンピュテーション式で行えることは一つ

  • trap { it (式) } と記述する
  • Delay によって式部分の実行は遅延されている
exception MyException

let `exn test` = test {
  let f () =
    raise MyException
    42
  let! e = trap { it (f ()) }
  do! assertEquals "" e.Message
  do! assertEquals typeof<MyException> (e.GetType())
  do! assertEquals "" (e.StackTrace.Substring(0, 5))
}

Quoteメソッド

  • Quoteを定義するとある部分で式木が返るようになる
  • 式木はλ式や変数、式本体などに分解・解析が可能
  • つまり色々仕込める
  • Runメソッドで式木を実行してしまえば、見かけ上はQuote適用前と同じ型にみえる

Quote悪用事例

  • 型拡張を使って二つのメソッドをビルダーにはやす
    • Quote
    • Run: Expr<'A> -> TestCase<'A>
  • これを定義したモジュールをインポートする
open Persimmon
open Persimmon.Pudding.Quotations // 既存のテストコードにこれを追加するだけ
open UseTestNameByReflection

let ``return int`` = test {
  return 1
}

let ``fail test`` = test {
  let! a = ``return int``
  do! assertEquals 2 a
  return a
}

このテストを実行すると

.x
Assertion Violated: fail test
1. [parameter]
     _arg1: System.Int32 -> 1
     _arg2: Microsoft.FSharp.Core.Unit -> <null>
     a: System.Int32 -> 1
   [method call]
     Persimmon.TestBuilder.Return(1) -> TestCase<Int32>({Name = "";
    Parameters = [];})
     assertEquals(2, 1) -> NotPassed (Violated "Expect: 2
   Actual: 1")

2. Expect: 2
   Actual: 1
============================== summary ===============================
run: 2, error: 0, violated: 1, skipped: 0, duration: 00:00:00.3012002

importしない場合は2.の結果しか表示されない

まとめ

  • コンピュテーション式はいろいろできる
    • そこそこ低コストで
  • あえてモナド専用にする理由はない
    • とはいえ世の中モナドは色々と出回っているので利用しやすいのは確か
  • モナドわかりません
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment