F#
プログラミング
VisualStudio
関数型言語
.NETFramework

tryとmatchの皮算用 - 例外処理か、振り分けか

F#はMicrosoftのDon Syme氏が開発したプログラミング言語で、命令型、オブジェクト指向、関数型の要素を併せ持つマルチパラダイム言語と位置付けられています。実装はオープンソースのMITライセンスのもと、githubのリポジトリで公開されています。

実行環境は.NET Frameworkおよび.NET CoreやMonoといったマルチプラットフォームに対応していますが、サポート状況については各プラットフォームごとの情報を参照してください(「F# の概要」など)。

なお、ここで対象とするF#のバージョンは4.1(FSharp.Core 4.4.1.0)とします。

マルチパラダイムだからこそ...

F#はマルチパラダイムのプログラミング言語だからこそ抱える葛藤があります。それはどんなスタイルでコーディングするかです。ここでは例外処理に関して、それぞれのスタイルを考えてみます。

例外処理のスタイル

例外処理とは、通常の処理をこれ以上続けられなくなったときに行う処理のとことですが、F#では大きく分けて2つのスタイルが考えられます。それは例外オブジェクトの発生処理の振り分けです。

例外オブジェクトの発生(try-with, try-finally)

List.find関数はリストの中から目的の値を見つける処理を行います。値が見つかれば、それが戻り値となりますが、値が見つからないときは「System.Collections.Generic.KeyNotFoundException」という例外オブジェクトを発生させます。

例外オブジェクトが発生したときはtry-withtry-finallyを使うと例外処理に移行できます。これらがなければ処理がその場で停止されます。

例外オブジェクトの発生(例外処理のスタイル)
try
  [1..5]
  |> List.find (fun n -> n < 0)
  |> printfn "%dを見つけました"  // List.findで例外オブジェクトが発生しなければ実行
with
  :? System.Collections.Generic.KeyNotFoundException
     -> printfn "見つかりませんでした"  // List.findで例外オブジェクトが発生したら実行

処理の振り分け(match-with, if-then)

List.tryFind関数もリストの中から目的の値を見つける処理を行いますが、値が見つかればSome n(nが見つかった値)、値が見つからなければNoneとなります。その後の処理は関数の結果をもとにmatch-withif-thenで処理を振り分けます。

処理の振り分け(例外処理のスタイル)
match [1..5] |> List.tryFind (fun n -> n < 0) with  // 処理の振り分け
| Some n -> printfn "%dを見つけました" n   // 通常処理
| None   -> printfn "見つかりませんでした"  // 例外処理

例外オブジェクトによる例外処理

例外オブジェクトとは、例外の内容を表すもので、System.Exceptionクラス(F#ではexn)を継承するサブクラスで表されます。.NET環境ではさまざまな例外オブジェクトが定義済みで、自動的にこれらが生成されるようになっているメソッドもたくさん提供されています。

例外処理への移行

例外が発生した後、例外処理に移行するにはtry-withtry-finallyを使います。両者は役割が異なり、同時に使うこともあります。

try-withによる例外処理

try-withtryからwithまでの間で行われる処理の途中で例外が発生するとwith以下の処理に移行します。with以下では、例外オブジェクトの内容に応じて処理を振り分けます。

重要なのは、try-with間で行われた処理の結果とwith以下の処理の結果とが同じ型でなければならないということです。

以下はList.find関数の実行結果を得る処理ですが、try-with間の結果の型がintとなるため、with以下の結果の型もintとなるようにしなくてはなりません。

try-with間とwith以下の結果の型は同じにする
try
  [1..5] |> List.find (fun n -> n < 0)  // 見つかれば結果はint
with
  | _ -> -1  // 見つからなくても結果をintにする

with以下での処理の振り分け方はmatch-withのパターンマッチとよく似ています。match-withとの違いは、パターンマッチの対象が例外オブジェクトであることと、パターンに当てはまらない例外オブジェクトが発生したときに処理が停止する場合があることです。

以下は画面から入力されたファイル名のテキストファイルを読み込み、内容を表示する処理です。

with以下の処理の振り分け
open System
open System.IO

try
  (* 指定されたテキストファイルの内容を表示 *)
  printf "ファイル名を入力してください = "
  Console.ReadLine() |> File.ReadAllText |> printfn "%s"
with
  (* 例外処理の振り分け *)
  | :? ArgumentException  // ファイル名が入力されていない
       -> printfn "ファイル名を入力してください"
  | :? FileNotFoundException as ex  // 指定されたファイルが見つからない
       -> printfn "%s" ex.Message
  | ex -> printfn "%A" ex  // その他の例外(アクセス不能など)

ファイル名が入力されていないときはSystem.ArgumentExceptionが発生するため、with以下の:? ArgumentExceptionというパターンで振り分けられた処理を行います。:? FileNotFoundException as exは、ファイルが見つからなかったときのパターンで、例外処理で使う例外オブジェクトをexで表すようにしています。これら以外の例外が発生したときは、例外オブジェクト(ex)の内容を文字列化して表示します。

try-finallyによる例外処理

try-finallytry-finally間の処理で例外が発生していても、発生していなくてもfinally以下の処理を行います。この処理はwith以下のようなパターンマッチは行われません。

以下はtry-finally間でList.find関数を実行する処理ですが、finally以下は値が見つかっても、見つからなくても実行されます。

try-finallyによる例外処理
try
  // try-finally間では例外が発生する可能性のある処理を実行
  [1..5]
  |> List.find (fun n -> n < 0)
  |> printfn "%dを見つけました"
finally
  // finally以下は値が見つかっても見つからなくても実行される
  printfn "処理は終了しました"

try-withとtry-finallyの組み合わせ

try-finally内でtry-withをネストさせると、例外が発生したときにwith以下の処理を行った後、finally以下の処理が行われます。両者のネストを逆にすると、例外が発生したときにfinally以下の処理を行った後、with以下の処理が行われます。

try
  try
    [1..5]
    |> List.find (fun n -> n < 0)
    |> printfn "%dを見つけました"
  with
    // 例外が発生したときに実行される
    :? System.Collections.Generic.KeyNotFoundException
       -> printfn "見つかりませんでした"
finally
  // 例外が発生してもしなくてもtry-withの後で実行される
  printfn "処理は終了しました"

例外を発生させる関数

例外処理では、ただ発生した例外オブジェクトを受け止めるだけではなく、関数で例外オブジェクトを発生させることもできます。そうした関数には、発生させる例外オブジェクトの種類が決まっているものと、引数で種類を指定するものとがあります。発生させる例外オブジェクトの種類は処理の状況に合ったものも選択してください。

nullArg パラメータ名
string -> 'T

System.ArgumentNullException(引数がnullの例外)を発生させます。引数のパラメータ名は例外オブジェクトのメッセージに組み込まれます。

nullArg "arg1"

(* 発生する例外 *)
System.ArgumentNullException: 値を Null にすることはできません。
パラメーター名:arg1

invalidArg パラメータ名 メッセージ
string -> string -> 'T

System.ArgumentException(引数の例外)を発生させます。引数のパラメータ名とメッセージは例外オブジェクトのメッセージに組み込まれます。

invalidArg "arg1" "引数を設定してください"

(* 発生する例外 *)
System.ArgumentException: 引数を設定してください
パラメーター名:arg1

invalidOp メッセージ
string -> 'T

System.InvalidOperationException(操作の例外)を発生させます。引数のメッセージは例外オブジェクトのメッセージに組み込まれます。

invalidOp "この操作はできません"

(* 発生する例外 *)
System.InvalidOperationException: この操作はできません

failwith メッセージ
string -> 'T

例外オブジェクトを発生させ、with以下ではFailure msg(msgはメッセージ)のパターンで処理を振り分けられるようにします。そのため、例外処理では例外オブジェクト全体ではなくメッセージのみ使えます。

try
  failwith "Error!!"  // with以下の処理で使うメッセージ
with
  Failure msg -> printfn "%s" msg  // "Error!!"

failwithf 書式 値 ...
Printf.StringFormat<'T, 'Result> -> 'T

failwithにメッセージの書式を設定する引数が追加されたものです。引数の書式と値に基づいて編集されたメッセージがFailure msgmsgに設定されます。

try
  failwithf "エラー (%s)" <| System.DateTime.Now.ToString()
with
  Failure msg -> printfn "%s" msg  // "エラー (2018/01/21 22:20:11)"

raise 例外オブジェクト
System.Exception -> 'T

例外を発生させる関数で最も汎用的なものです。例外オブジェクトは引数で生成しておかなくてはなりません。

try
  raise <| System.NotSupportedException "サポートしていません"
with
  :? System.NotSupportedException as ex -> printfn "%s" ex.Message  // サポートしていません

reraise
unit -> 'T

同じ例外オブジェクトを発生させる関数です。例外処理を呼び出し元の関数やメソッドに委ねるときに実行します。最初に発生した場所は例外オブジェクトのStackTraceプロパティやTargetSiteプロパティに記録されます。

let g () =
  try
    failwith "gのエラー" |> ignore  // 最初の例外オブジェクト発生
  with
    _ -> reraise()  // 呼び出し元fで同じ例外オブジェクトを発生させる

let f () =
  try
    g ()  // reraise() で例外オブジェクト発生
  with
    ex -> printfn "%A" ex  // reraise()で発生した例外オブジェクト

f()

例外オブジェクト型の定義

例外オブジェクト型はSystem.Exceptionクラスのサブクラスですが、これを通常の継承とは違うexceptionを使って定義できます。これを使うと型の定義が短くて済み、オブジェクトに持たせる値の型も柔軟に定義できます。

たとえばint * string型の値持つ例外オブジェクトCustomExceptionは以下のように定義できます。

例外を表す型の定義
exception CustomException of int * string  // of 以下が例外オブジェクトの型

こうして定義したCustomExceptiontry-with間やtry-finally間でraise関数によって例外として発生させることができます。そしてwith以下のパターンマッチでこの例外への対応を振り分けられらます。

例外オブジェクトの生成とパターンマッチ
try
  // 例外が発生させる処理
  CustomException (1, "Error!!") |> raise
with
  // exceptionで定義した例外のパターンマッチ
  | CustomException(n, s) -> printfn "%d %s" n s  // raise実行時に持たせた値を使える

処理の振り分けによる例外処理

例外処理とはいっても例外オブジェクトを使わず、これまでの結果を元に処理を振り分けるときもあります。振り分けはmatch-withif-then-elseで行いますが、結果を表す値にはさまざまな型が使われます。ここではそのうちのいくつかを紹介します。

'T option (Some result / None)

'T optionSome result(resultが結果)とNoneという値をもつ型です。リスト内の値を探索するList.tryFind関数のように、結果が存在するときもあればそうでないときもある場合に使われます。

match-withで処理を振り分けると、値が見つかったときはSome resultからresultだけを取り出して処理を続行でき、Noneのときには例外処理を行うようにできます。

optionとmatch-withによる処理の振り分け
match [1..5] |> List.tryFind (fun n -> n > 2) with
| Some result -> printfn "%dを見つけました" result  // 通常処理
| None -> printfn "値は見つかりませんでした"         // 例外処理

bool (true / false)

trueもしくはfalseのどちらかで処理を振り分ける方法もあります。System.Int32.TryParseメソッドでは、文字列を数値に変換出来たら(true, 数値)、変換できなかったら(false, _)という結果になります。そのため、タプルの左の要素で結果の可否を判定して処理を振り分けます。

bool値で処理を振り分け
let s = "123"
match System.Int32.TryParse s with
| (true, n)  -> printfn "%d に変換できました" n
| (false, _) -> printfn "変換できませんでした"

アクティブパターン(Choice)

アクティブパターンは結果に応じた識別子を自動的に設定できるもので、match-withとともに使うと処理の振り分けができます。識別子は最大7つまで定義できます。アクティブパターンの戻り値の型はChoiceになります。

アクティブパターンによる処理の振り分け
let (|Good|Bad|) n = if n >= 70 then Good else Bad  // int -> Choice<unit,unit>

let score = 73    // 点数
match score with  // 合否判定
| Good -> "合格"
| Bad  -> "不合格"

アクティブパターンでは識別子のどちらにも値を持たせられます。それぞれの値の型は違っていても対応できます。

値を持つアクティブパターンによる処理の振り分け
let (|Good|Bad|) n = if n >= 70 then Good n else Bad "不合格"  // int -> Choice<int,string>

let score = 30    // 点数
match score with  // 合否判定
| Good n -> printfn "%d点で合格" n
| Bad  s -> printfn "%s" s

これに'T optionを組み合わせたパーシャルアクティブパターンでは、結果がSome resultのときは識別子が設定され、Noneのときには識別子が設定されません。パーシャルアクティブパターンの戻り値の型は'T optionになります。

パーシャルアクティブパターンによる処理の振り分け
let (|Good|_|) n = if n >= 70 then Some n else None  // int -> int option

let score = 73    // 点数
match score with  // 合否判定
| Good s -> printfn "%d点は合格" s
| _      -> printfn "不合格"

Result<'Succ, 'Fail>

ResultOk result(resultは結果)とError fail(failは例外を表す値)のどちらかを表す型です。resultfailの型は違っていても対応します。識別子を見ただけで結果が正常かエラーかが分かるのが特徴です。

Resultで処理を振り分け
let check n = if n >= 70 then Ok n else Error "不合格"  // int -> Result<int,string>

let score = 20
match check score with
| Ok n    -> printfn "%d点で合格" n
| Error s -> printfn "%s" s

例外処理のスタイルを、例外オブジェクトの発生と処理の振り分けとで考えてみました。

ここをご覧いただいた方々の参考になればと思います。