(hatena (diary ’Nobuhisa)) このページをアンテナに追加 RSSフィード Twitter

10/12/07 : SENSE

[]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

実行結果:

どーん
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<float,float> * Either<float,float> = (Right 255.0, Left 8.0)

> test_either () ;;
val it : Either<string,int> * Either<string,int> * Either<string,int> =
  (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 = ()

groundground 2010/12/07 12:20 質問なのですが、try-catch-finallyは次のように3段階になってると思います。
try {
// ファイルに対する処理
}
catch(IOException){
// 失敗した時の処理
}
finally {
// ファイルを閉じる。
}

Eather版だと、例外をキャッチできない場合、finallyの部分が実行されないような気がするのですが、
それだと問題が無いでしょうか。(たとえばNullReferenceExceptionで死んだ時)など

NobuhisaNobuhisa 2010/12/08 19:46 groundさんコメントありがとうございます。

おっしゃる通り、記事に載せていた例はあらゆる例外をキャッチすることを想定していませんでした。
コメントを受けて、全ての例外に対応するやり方も追記しておきました。よろしかったらご覧ください。

ただ、それぞれの例外にも意味があるわけですから、あらゆる例外をキャッチするというのは例外設計上あまり推奨されるやり方ではないかなという気もします。
想定されるエラーをEitherに置き換えるのがしっくり来るかなと思います。

mzpmzp 2010/12/08 21:40 ステキだと思います。

全部の関数がEitherで返してくれるわけじゃないので

// 例外を投げる関数→Eitherを返す関数
let sure f x =
try
Right (f x)
with e ->
Left e

みたいな高階関数をよく使ってます。

NobuhisaNobuhisa 2010/12/10 01:40 みずぴーさんコメントどうも!Advent Calendar楽しみにしてます!

やっぱり変換してくれるような関数を最初から準備しておいた方が便利ですよね。
参考になります。

ただ、.NETの場合は例外投げるのに結構コストがかかるので、一度発生させてからラップする方法は多用すべきではないかもしれません。
面倒でも最初からEitherを返す関数を準備するのが.NET的にはいいのかな・・・。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/Nobuhisa/20101207/p1