CC BY-SA 4.0
- なかやん・ゆーき / ぺんぎん / もみあげ
- @pocketberserker / id:pocketberserker
- Microsoft MVP for
F#.NET (2015/04/01~ 2016/03/31) - モナドわかりません
- .NET向け関数プログラミング言語
- 7月に 4.0 がリリースされた
- ひと安心?
- VS2015ではオプショナルインストールだけどな!(これはC++もなのでVisual Studioの方針の問題)
- 現実をみて色々と割り切ってる(例: 非変のみ)
- 人によっては中途半端に感じるかもしれない
- でもそこが可愛いのですよ
- 日本での利用率はそんなに高くない
- カスタマイズポイントを提供した式
- 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特化とかではない
http://www.slideshare.net/bleistift/yieldreturn
yield
とreturn
について考察した資料- StateやContを使ってコンピュテーション式を実装すると色々制御できるよという話ものっている
- 参考: https://github.com/BasisLib/Basis.Core
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によるコンピュテーション式の解析
- (型拡張、継承を用いた構文捻じ曲げ) 今回の対象外
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
}
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# のクエリ式はカスタムオペレータを使って実装されている
パラメタライズドテストを表現するために利用
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を定義するとある部分で式木が返るようになる
- 式木はλ式や変数、式本体などに分解・解析が可能
- つまり色々仕込める
- Runメソッドで式木を実行してしまえば、見かけ上は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.
の結果しか表示されない
- コンピュテーション式はいろいろできる
- そこそこ低コストで
- あえてモナド専用にする理由はない
- とはいえ世の中モナドは色々と出回っているので利用しやすいのは確か
- モナドわかりません