M.Hiroi's Home Page

お気楽 F# プログラミング超入門

例外


Copyright (C) 2022 Makoto Hiroi
All rights reserved.

はじめに

「例外 (exception)」は主にエラー処理で使われる機能です。「例外=エラー処理」と考えてもらってもかまいません。Common Lisp には「コンディション (condition)」という例外処理があります。近年、例外処理をサポートしているプログラミング言語が多くなりました。もちろん、F# にも例外処理があります。

●例外の定義

F# にはあらかじめ定義されている例外があります。たとえば、次の例を見てください。

> 4 / 0;;
System.DivideByZeroException: Attempted to divide by zero.
...略...

> List.tail (List.tail [1]);;
System.ArgumentException: 入力リストが空でした。 (Parameter 'list')
...略...

F# では、例外処理を何も行っていなければ、処理を中断して例外の種類を表示します。たとえば、最初の例のように 0 で除算すると例外 DivideByZeroException が発生します。次の例では、空リストに tail を適用した結果、例外 ArgumentException が発生します。

.NET の場合、例外は階層構造になっていて、すべての例外はクラス Exception を継承しています。クラスや継承などオブジェクト指向についてはあとで詳しく説明しますが、F# でも Exception がもっとも基本的な例外になります。

なお、例外が発生したことを、「例外が送出された」という場合もあります。本稿では、「例外を送出する」とか「例外が送出された」と記述することにします。

例外は exception を使って、ユーザが独自に定義することができます。

exception 名前
exception 名前 of 型式

たとえば、exception Foo とすると例外 Foo が定義されます。F# の場合、例外の名前を英大文字から始めます。例外に引数を渡す場合は型式を指定します。たとえば、exception Bar of int * int とすると、例外 Bar に整数を 2 つ渡すことができます。

それでは、実際に定義してみましょう。

> exception Foo;;
exception Foo

> Foo;;
val it: exn = Foo

> exception Bar of int * int;;
exception Bar of int * int

> Bar (1, 2);;
val it: exn = Bar (1, 2)

F# の場合、例外は exn というバリアント型で表されていて、例外の名前は exn 型のコンストラクタになります。バリアントの場合、定義したあとでコンストラクタを追加することはできませんが、exn 型に限ってコンストラクタを追加することができるようになっています。

●例外の送出

例外を送出するには raise を使います。

raise 例外

たとえば、raise Foo とすれば例外 Foo が送出されます。raise (Bar (1, 2)) とすれば、例外 Bar が送出されます。次の例を見てください。

> let foo n = if n < 0 then raise Foo else 1;;
val foo: n: int -> int

> foo 1;;
val it: int = 1

> foo (-1);;
FSI_0018+Foo: Exception of type 'FSI_0018+Foo' was thrown.
...略...

> let bar a b = if a < 0 || b < 0 then raise (Bar (a, b)) else 1;;
val bar: a: int -> b: int -> int

> bar 1 1;;
val it: int = 1

> bar (-1) 1;;
FSI_0020+Bar: Exception of type 'FSI_0020+Bar' was thrown.
...略...

関数 foo n は、n < 0 の場合に例外 Foo を送出し、それ以外の場合は 1 を返します。関数 bar a b は、a < 0 または b < 0 の場合に例外 Bar を送出し、それ以外の場合は 1 を返します。if E then F else G は式 F と G の返り値が同じデータ型でなければいけませんが、式 F が例外を送出する raise なので、式 G のデータ型は何でもかまいません。

送出する例外が Exception でよければ、関数 failwith を使うと簡単です。

> failwith;;
val it: (string -> 'a)

> if false then 1 else failwith "oops!";;
System.Exception: oops!
...略...

failwith はエラーメッセージを引数に受け取り、例外 Excepion を送出したときエラーメッセージを表示します。

それでは簡単な例題として、階乗を求める関数 fact に引数をチェックする処理を追加してみましょう。次のリストを見てください。

リスト 1 : 階乗

exception Negative

let fact n =
  let rec facti n a =
    if n = 0 then a
    else facti (n - 1) (n * a)
  if n < 0 then raise Negative
  else facti n 1

最初に exception で例外 Negative を定義します。そして、局所関数 facti を呼び出す前に引数 n の値をチェックします。n < 0 であれば raise で例外 Negative を送出します。raise Negative のかわりに、failwith を使ってもかまいません。簡単な実行例を示します。

> fact 10;;
val it: int = 3628800

> fact (-1);;
FSI_0041+Negative: Exception of type 'FSI_0041+Negative' was thrown.

●例外の捕捉

F# の場合、送出された例外を「捕捉 (catch)」することで、処理を中断せずに継続することができます。処理をやり直したい場合や特別なエラー処理を行いたい場合、例外処理はとても役に立ちます。F# では、次の式で例外を捕捉することができます。

try 式0 with
  パターン1 -> 式1
| パターン2 -> 式2
  ...
| パターンn -> 式n

例外を捕捉するには try 式を使います。式 0 の処理で例外が送出されると、その例外とマッチングするパターンの節を選択し、対応する式を評価します。そして、その結果が try 式の返り値となります。式 0 が正常に終了した場合は、その評価結果が try 式の返り値になります。

簡単な例を示しましょう。次のリストを見てください。

リスト 2 : 例外の捕捉

exception Foo of int

let foo n = if n < 0 then raise (Foo n) else 1  

let foo1 n =
  try foo n with
    Foo (-1) -> printfn "Foo error 1"; 0
  | Foo (-2) -> printfn "Foo error 2"; 0
  | Foo _ -> printfn "Foo error 3" ; 0

関数 foo は引数 n が負の場合に例外 Foo を送出します。関数 foo1 は foo を呼び出し、例外が送出された場合は try で捕捉します。例外の引数はパターンマッチングで取り出すことができます。Foo (-1) の場合は "Foo error 1" を表示して 0 を返します。関数 foo の返り値は int なので、例外を捕捉したときに評価する式の返り値も int でなければいけません。ご注意ください。

Foo (-2) の場合は "Foo error 2" を表示して 0 を返します。最後のパターンは匿名変数が使われているので、その他の数値はこの規則とマッチングします。"Foo error 3" を表示して 0 を返します。

それでは、実行してみましょう。

> foo (-1);;
FSI_0046+Foo: Exception of type 'FSI_0046+Foo' was thrown.
...略...

> foo1 2;;
val it: int = 1

> foo1 (-1);;
Foo error 1
val it: int = 0

> foo1 (-2);;
Foo error 2
val it: int = 0

> foo1 (-4);;
Foo error 3
val it: int = 0

foo (-1) をそのまま評価すると例外を送出します。foo1 (-1) を評価すると foo が送出した例外を捕捉して、エラー処理を行ってから 0 を返しています。foo1 (-2) も foo1 ( -4) も例外 Foo を捕捉しています。このように、例外を捕捉して処理を続行することができます。

failwith が送出する例外とパターンマッチするときはパターンに Failure を使います。

> let foo n = if n >= 0 then 1 else failwith "oops! negative number";;
val foo: n: int -> int

> try foo 1 with Failure (mes) -> printfn "%s" mes; 0 | _ -> printfn "unknown error"; -1;;
val it: int = 1

> try foo (-1) with Failure (mes) -> printfn "%s" mes; 0 | _ -> printfn "unknown error"; -1;;
oops! negative number
val it: int = 0

●問題

中置記法で書かれた数式を計算するプログラムを作ってください。数式はリストで表すことにします。リストの要素は次のように定義します。

type item = Add | Sub | Mul | Div | Rpa | Lpa | N of float

演算子は Add (+), Sub (-), Mul (*), Div (/) で、数値は実数 (float) だけとします。数式はカッコを使うことできます。右カッコを Rpa で、左カッコを Lpa で表します。

exception Expr_err
val expression: xs: item list -> float
> expression [N 1.; Add; N 2.; Add; N 3.; Add; N 4.];;
val it: float = 10.0

> expression [N 1.; Add; N 2.; Mul; N 3.; Add; N 4.];;
val it: float = 11.0

> expression [Lpa; N 1.; Add; N 2.; Rpa; Mul; Lpa; N 3.; Add; N 4.; Rpa];;
val it: float = 21.0



















●解答

参考文献『C言語による最新アルゴリズム事典』の「式の評価」によると、四則演算の数式は次の構文規則で表すことができます。

式 := 項 (+ | -) 項 (+ | -) 項 ...
項 :- 因子 (* | /) 因子 (* | /) 因子 ...
因子 := 数 | (式)

これをそのままプログラムすると、次のようになります。

リスト : 数式の計算 (中置記法)

(* 例外の定義 *)
exception Expr_err

type item = Add | Sub | Mul | Div | Rpa | Lpa | N of float

let rec factor = function
  (N x)::xs -> (x, xs)
| Lpa::xs -> let (v, ys) = expr xs in
               if List.head ys = Rpa then (v, List.tail ys)
               else raise Expr_err
| _ -> raise Expr_err
and term xs =
  let rec term_sub value = function
    [] -> (value, [])
  | Mul::xs -> let (v, ys) = factor xs in term_sub (value * v) ys
  | Div::xs -> let (v, ys) = factor xs in term_sub (value / v) ys
  | xs -> (value, xs)
  let (v, ys) = factor xs
  term_sub v ys
and expr xs =
  let rec expr_sub value = function
    [] -> (value, [])
  | Add::xs -> let (v, ys) = term xs in expr_sub (value + v) ys
  | Sub::xs -> let (v, ys) = term xs in expr_sub (value - v) ys
  | xs -> (value, xs)
  let (v, ys) = term xs 
  expr_sub v ys

let expression xs =
  let (v, ys) = expr xs in
    match ys with
      [] -> v
    | _ -> raise Expr_err

関数 expr は「式」を評価します。実際の処理は局所関数 expr_sub で行います。最初に関数 term を呼び出して「項」を評価します。返り値はタプルで、値は評価結果 v と残りのリスト ys です。演算子が Add (+) または Sub (-) の場合、term を呼び出して式 xs を評価し、返り値を v と ys にセットします。そして、value と v を加算 (または減算) して expr_sub を再帰呼び出しします。そうでなければ、評価結果 x と残りのリスト xs をタプルで返します。

関数 term も同様の処理を行います。この場合は最初に関数 factor を呼び出して「因子」を評価します。そして、演算子が Mul (*) または Div (/) の場合は factor を呼び出して評価を続行します。そうでなければ、評価結果 x と残りのリスト xs をタプルで返します。関数 factor は簡単で、引数の先頭要素が数値の場合はそれをそのまま返し、Lpa であれば xs を expr に渡して評価します。戻ってきたら、リスト ys の先頭要素が Rpa であることを確認します。それ以外の場合はエラーを送出します。

最後に、関数 expression から expr を呼び出します。リスト ys が空リストでなければ式に誤りがあるのでエラーを送出します。そうでなければ計算結果 v を返します。

-- 参考文献 --------
[1] 奥村晴彦,『C言語による最新アルゴリズム事典』, 技術評論社, 1991

初版 2022 年 3 月 12 日