ScalaとHaskellがF#に救いの手を

F# Advent Calendar jp 2010 第1回目!


F#の弱点(?)のひとつに、C#でいう try-catch-finally の仕組みが無いという点が挙げられます。
F#ではどのようにするかというと、try-with と try-finally をネストすることによって解決します。
「この、ねすとねすとした感じ、どうにかなりませんか!」


この問題を解決するヒントが、ScalaHaskellにあります。
ご存知(?)Either型!

最も単純なEitherの定義

のちにもっと便利な関数群を定義するとして、今は基本形だけを定義してみます。

type Either<'T, 'U> =
    | Left  of 'T
    | Right of 'U

たったこれだけでエラー(例外)処理に変化が生まれます。

open System

let fac' n = List.fold ( * ) 1 [1 .. n]

(* 普通にinvalidArgで例外を発生させる関数 *)
let fac_Exception = function
    | n when n < 1 -> invalidArg "n" "どーん"
    | n            -> fac' n

(* Eitherで例外をラップして返す
   型は fac_Either : int -> Either<ArgumentException,int> *)
let fac_Either = function
    | n when n < 1 -> Left (ArgumentException "えらああ!")
    | n            -> Right (fac' n)

次にこのエラーを処理する側の関数。

(* ねすとねすとしてる *)
let test_Exception () =
    try
        try
            (* トビウオに擬態したいぐらいねすとねすとしてる! *)
            fac_Exception 0 |> printfn "result : %d"
        with
        | e -> printfn "%s" e.Message
    finally
        printfn "おしまい"

(* ねすとねすとしてない・・・! *)
let test_Either () =
    match fac_Either 0 with
    | Left e  -> printfn "%s" e.Message // 例外のメッセージを表示
    | Right x -> printfn "result : %d" x
    printfn "end"


[<EntryPoint>]
let main _ =
    test_Exception ()
    printfn "====="
    test_Either ()
    printfn "====="
    0

実行結果:

どーん
Parameter name: n
おしまい
[]=====[]
えらああ!
end
[]=====[]
Rightには「正しい」という意味もあるので、エラーはLeftに包ませるのが慣用のようです。インド人みたい!
エラーに特化した使い方をするのなら、別名で定義した方がよいかもしれませんね。
また、.NETでは例外をスローするのは結構コストがかかるので、「例外をスロー」していないEither版の方が動作は高速かと思います。

Arrow再び

今度は、Eitherをもっと便利にするための関数群を定義しようと思います。
HaskellのArrowChoiceからヒントを得ました。(実は以前もF#版Arrowは登場してます
また、MaybeモナドのEither版のようなコンピューテーション式も用意しました。

open System

type Either<'T, 'U> =
    | Left  of 'T
    | Right of 'U

    member self.Swap =
        match self with Left x -> Right x | Right x -> Left x


module Either =
    /// Rightには『正しい』の意味もあるので、testがtrueの時にRightを返す
    let inline cond test left right =
        if test then Right right else Left left

    let inline mapLeft f = function Left x -> Left (f x) | r -> r
    let inline mapRight f = function Right x -> Right (f x) | l -> l
    (* Arrowの(+++) *)
    let inline mapEither f g = mapLeft f >> mapRight g
    (* Arrowの(|||) *)
    let inline map f g =
        mapEither f g >> (function Left x -> x | Right x -> x)
    let inline apply f g = (function Left x -> f x ; () | Right x -> g x)

    type EitherBuilder() =
        member self.Bind (expr, f) = expr |> (function Right x -> f x | x -> x)
        member self.Return (x) = Right x
        member self.Delay (f) = f ()

    let either = new EitherBuilder()

いくつか動作例を挙げてみます。

let inline flip f y x = f x y

(* 右だけいじくり回す *)
let test_map () =
    let f = Either.mapRight (( ** ) 2.0) >> Either.mapRight (flip ( - ) 1.0)
    f (Right 8.0), f (Left  8.0)

(* コンピューテーション式の例。fun1,fun2の両方をクリアしないと結果が返らない。 *)
let test_either () =
    let fun1 x = if x % 2 = 0 then Right (x + 1) else Left ("error")
    let fun2 x = if 0 <= x then Right (x * 10) else Left ("えらー")
    let f x = Either.either {
        let! a = fun1 x
        let! b = fun2 a
        return b
    }
    (f 1, f -2, f 4)

対話環境で試してみると・・・

> test_map () ;;
val it : Either * Either = (Right 255.0, Left 8.0)

> test_either () ;;
val it : Either * Either * Either =
(Left "error", Left "えらー", Right 50)

Optionだとエラーの詳細を持ち運ぶことができないので、もっと色々連れ回したい場合はEitherの方が便利ですね。


最後に、冒頭のfac_Either関数をArrowで。

(* 再掲 *)
let fac' n = List.fold ( * ) 1 [1 .. n]
let fac_Either = function
    | n when n < 1 -> Left (ArgumentException "えらああ!")
    | n            -> Right (fac' n)

let test_Arrow () =
    fac_Either 0
    |> Either.apply (printfn "えらー : %A") (printfn "こたえ : %d")
    printfn "えんど" // finallyな処理

最初はねすとねすとしていた関数も、ここまでスッキリしました。
では、F# Advent Calendar 2番手にバトンタッチ!


コメントを受けての追記

予期せぬ例外が発生した場合、Either版だとキャッチできなくてfinallyが実行されないじゃないか!
とのコメントを頂きましたので、あらゆる例外を捕獲する関数の例を載せておきます。

(* 例外の発生を遮断する関数 *)
let abort f x = try Right (f x) with e -> Left e
(* 適当に例外を投げる関数 *)
let onError1 = function true -> NullReferenceException() |> raise | _ -> ()

let test x =
    abort onError1 x
    |> Either.apply (fun _ -> printfn "catch!")
                    (fun _ -> printfn "try!")
    printfn "finally!"

対話環境でテストしてみます。

> test false ;;
try!
finally!
val it : unit = ()
> test true ;;
catch!
finally!
val it : unit = ()