F#でAsyncとResultを組み合わせたときにきれいに書く方法

スピーダ事業Product Teamのあやぴーです。

「関数型ドメインモデリング」が翻訳されて日本でもF#が流行る兆しが見えてきたので、今日はF#を書き始めた人が感じやすい違和感を解決する方法についての紹介です。尚、私たちProduct Team内では持っていない人はいないのではないか、と思う程度には購入している人が多い本です。

store.kadokawa.co.jp

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 を束縛して、 ab で割るというコードです。見ると分かりますが、ただ関数を呼び出した結果同士で割り算をするにしてはやけに仰々しいコードになりました。

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を使ってみることにしましょう。

github.com

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 コンピュテーション式を使うと AsyncResult を組み合わせたコードでもきれいにコードが書けるよ、という話でした。

おまけ

これは余談なんですが、元々FsToolkit.ErrorHandlingを知らなかったときに、以下の記事を読んでいて「モナドトランスフォーマーを使えばいいのか」と納得したので、モナドトランスフォーマーを使う例を書いておきます。

xuwei-k.hatenablog.com

FSharpPlusというライブラリを使うと monad コンピュテーション式と ResultT が使えるので、先程の例は以下のように書けます。

github.com

#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を入れるのであれば、モナドトランスフォーマーを活用してしまえばいいなとは思っていたりします。私自身はモナドやモナドトランスフォーマーを完全理解しているわけではないですが、使うこと自体はまあできるのでこういう取り入れやすいところから取り入れていけるといいのではないかなと考えています。

Page top