スピーダ事業Product Teamのあやぴーです。
「関数型ドメインモデリング」が翻訳されて日本でもF#が流行る兆しが見えてきたので、今日はF#を書き始めた人が感じやすい違和感を解決する方法についての紹介です。尚、私たちProduct Team内では持っていない人はいないのではないか、と思う程度には購入している人が多い本です。
F#には async
コンピュテーション式というものがあり、非同期処理をうまく書きやすいです。例えば、以下のようなコードです。
let asyncFunctionA () = async { return 42 } let asyncFunctionB () = async { return 2 } let result = async { let! a = asyncFunctionA () let! b = asyncFunctionB () return a * b } |> Async.RunSynchronously
また、他の言語でもあるように Result
型が用意されており、パターンマッチの組み合わせによって確実に分岐を網羅することができるようになっています(いわゆる代数的データ型の扱いに長けています)。
let result = Ok 42 match result with | Ok x -> x | Error _ -> failwith "Error"
それぞれについて一見すると便利な機能のように見えますし、実際かなり強力な機能で使い勝手はとてもよいです。ただ、これらを組み合わせるとなんだか冗長なコードが現れてきます。
次のコードは Async<Result>>
を返却する関数f,gの結果でそれぞれ a
, b
を束縛して、 a
を b
で割るというコードです。見ると分かりますが、ただ関数を呼び出した結果同士で割り算をするにしてはやけに仰々しいコードになりました。
let f(): Async<Result<int, unit>> = async { return Ok 42 } let g(): Async<Result<int, unit>> = async { return Error () } let x = async { let! a = f() match a with | Ok a' -> let! b = g() return match b with | Ok b' -> a' / b' |> Ok | _ -> Error "b is Error" | _ -> return Error "a is Error" } |> Async.RunSynchronously
関数 f
の結果を見ないで関数 g
を評価していい場合はもう少し簡素な書き方にはなります。
let x = async { let! a = f() let! b = g() return match a, b with | Ok a', Ok b' -> a' / b' |> Ok | Ok _, Error _ -> Error "b is Error" | Error _, _ -> Error "a is Error" } |> Async.RunSynchronously
ただ、やっぱりなんだか少し仰々しく感じます。F#が良い言語だと聞いて書き始めた人がかなり序盤で残念に感じるところだと思います。これを解決するために書籍「関数型ドメインモデリング」では AsyncResult
という Async<Result>
に対するエイリアスとこの Async<Result>
をうまく扱うための asyncResult
コンピュテーション式というものが提案されています。
書籍では asyncResult
の具体的な実装については触れていませんが、リポジトリを参照するとプロダクションではFsToolkit.ErrorHandlingなどのメンテナンスされているライブラリを使うよう勧められているので、今回はFsToolkit.ErrorHandlingを使ってみることにしましょう。
FsToolkit.ErrorHandlingが提供する asyncResult
コンピュテーション式を使うと以下のように書けます。
#r "nuget: FsToolkit.ErrorHandling, 4.16.0" open FsToolkit.ErrorHandling let x = asyncResult { let! a = f() |> AsyncResult.mapError (fun _ -> "a is Error") let! b = g() |> AsyncResult.mapError (fun _ -> "b is Error") return a / b } |> Async.RunSynchronously
だいぶすっきり書けるようになりました。というわけで、FsToolkit.ErrorHandlingが提供する asyncResult
コンピュテーション式を使うと Async
と Result
を組み合わせたコードでもきれいにコードが書けるよ、という話でした。
おまけ
これは余談なんですが、元々FsToolkit.ErrorHandlingを知らなかったときに、以下の記事を読んでいて「モナドトランスフォーマーを使えばいいのか」と納得したので、モナドトランスフォーマーを使う例を書いておきます。
FSharpPlusというライブラリを使うと monad
コンピュテーション式と ResultT
が使えるので、先程の例は以下のように書けます。
#r @"nuget: FSharpPlus" open FSharpPlus open FSharpPlus.Data let x = monad { let! a = f() |> map (Result.mapError (fun _ -> "a is Error")) |> ResultT let! b = g() |> map (Result.mapError (fun _ -> "b is Error")) |> ResultT return a / b } |> ResultT.run |> Async.RunSynchronously
これだけだと、別にFsToolkit.ErrorHandlingでいいじゃないかというように見えるんですが、モナドトランスフォーマーはより汎用的仕組みなので例えば以下のコードのように Async<Option>
を OptionT
にして扱ったり、あるいは Result<Option>
というものを OptionT
として扱う、といったこともできます。
let f (): Async<Option<int>> = async { return Some 42 } let g (): Async<int> = async { return 0 } let x = monad { let! a = f() |> OptionT let! b = g() |> map (fun x -> if x = 0 then None else Some x) |> OptionT return a / b } |> OptionT.run |> Async.RunSynchronously
FSharpPlusにはReaderモナドなども実装されており、便利に使えるので個人的にはどうせFSharpPlusを入れるのであれば、モナドトランスフォーマーを活用してしまえばいいなとは思っていたりします。私自身はモナドやモナドトランスフォーマーを完全理解しているわけではないですが、使うこと自体はまあできるのでこういう取り入れやすいところから取り入れていけるといいのではないかなと考えています。