ScalaとHaskellがF#に救いの手を
F# Advent Calendar jp 2010 第1回目!
F#の弱点(?)のひとつに、C#でいう try-catch-finally の仕組みが無いという点が挙げられます。
F#ではどのようにするかというと、try-with と try-finally をネストすることによって解決します。
「この、ねすとねすとした感じ、どうにかなりませんか!」
この問題を解決するヒントが、ScalaとHaskellにあります。
ご存知(?)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
実行結果:
どーんRightには「正しい」という意味もあるので、エラーはLeftに包ませるのが慣用のようです。インド人みたい!
Parameter name: n
おしまい
[]=====[]
えらああ!
end
[]=====[]
エラーに特化した使い方をするのなら、別名で定義した方がよいかもしれませんね。
また、.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 () ;;Optionだとエラーの詳細を持ち運ぶことができないので、もっと色々連れ回したい場合はEitherの方が便利ですね。
val it : Either* Either = (Right 255.0, Left 8.0) > test_either () ;;
val it : Either* Either * Either =
(Left "error", Left "えらー", Right 50)
最後に、冒頭の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 = ()