Edited at

「例外を投げない」という選択肢をとる言語

新しめの言語では例外を投げることを推奨しない言語が出てきているように思えるが、そうした言語が例外をどう考え、例外の代わりにどのようなアプローチを奨励しているかを調べてみた。

本稿での「例外」とは、Javaのthrow構文のようにスコープを脱出してcatchされるまでエスカレートされる「投げる例外」のことを指し、エラーを表現したオブジェクト(エラーオブジェクト)については「例外オブジェクト」と呼び区別するものとする。(この2つを同一に扱うと、例外を使わないということは、エラーオブジェクトは使わないの?という話になるため)


Go言語 - 例外はコードを複雑にする

Go言語では、通常、エラーは戻り値として扱われる。(本当の本当に例外的なエラーのためにpanic, recoverがあるが、ほとんど使われることがないように見受けられる。)

例外がないGoでは、どう呼び出し元にエラーを伝えているかというと、多値返却と呼ばれる複数の値を返す構文を使う。これはエラーハンドリングだけのためのものではない。値を複数返すうちのひとつに、エラーも返すことができるというだけだ。つまり、エラーもまた普通の戻り値と同じ地位で扱われるわけだ。

func Open(name string) (file *File, err error)

// ↑多値返却の宣言

file, err := os.Open("filename.ext") // ファイルとエラーの2つが値が同時に返る
if err != nil { // 他の言語のcatchのようなエラーハンドリング専用制御構文は使わない
log.Fatal(err)
}

公式ドキュメントになぜ例外を推奨しない言語設計にしているのか、その理由が書いてある:


Why does Go not have exceptions?

[なぜGoには例外がないのですか?]

We believe that coupling exceptions to a control structure, as in the try-catch-finally idiom, results in convoluted code. It also tends to encourage programmers to label too many ordinary errors, such as failing to open a file, as exceptional.

[例外をtry-catch-finallyのような制御構造に結びつけることは結果的に複雑なコードにつながると考えております。そして、それはファイルを開くのに失敗したなどの数多くの普通のエラーに例外としてラベル付することをプログラマーに推奨するものです。]

Frequently Asked Questions (FAQ) - The Go Programming Language


また、Go言語ではエラーハンドリングは発生した箇所で明示的に行うことを推奨している:


In Go, error handling is important. The language's design and conventions encourage you to explicitly check for errors where they occur (as distinct from the convention in other languages of throwing exceptions and sometimes catching them). In some cases this makes Go code verbose, but fortunately there are some techniques you can use to minimize repetitive error handling.

[Goではエラーハンドリングは重要です。言語の設計と規約では、エラーが発生した箇所で明示的にチェックすることを推奨しています(例外を投げ、キャッチすることがあるような他の言語の規約とは異なります)。そのためGoのコードが冗長になることがありますが、幸いながらエラーハンドリングの繰り返しを最小化するテクニックがあります。]

Error handling and Go - The Go Blog



Scala - 例外は副作用をもたらす

Scalaにはthrow、try-catchがあるが、Scalaのベストプラクティスではそれらの使用を推奨しない。

Scalaはオブジェクト指向言語でもあるが、同時に関数型言語の色も強い言語だ。関数型言語が「副作用がない」ことを重視するように、Scalaも副作用の無さを大事にする。副作用がないとは、関数(メソッド)がその戻り値以外に影響を与えない性質のことだ。関数型言語ではthrowされる例外は戻り値とは考えない。もし、関数が例外をthrowすると、戻り値以外に影響を与えてしまう。したがって、例外がthrowされる関数は副作用があり、良いものではないと考えられている。

Scalaでは、例外を投げる代わりに、例外を戻り値にする方法がよくとられる。例外を戻り値にする点では、Go言語と似ているが、Scalaは多値返却ではなく、「成功したときの値、もしくは、エラーを返す」といった形で戻り値を表現する。

ScalaではEitherTryを使う。Eitherは「どちらか」という意味の通り、2種類のうちどちらかの型になることを表現できるクラスだ。Tryもクラスで、SuccessまたはFailureどちらかの値になることを表現できる。

下記のコードのEitherは、Exception(エラー)もしくは、Fileオブジェクトを返す関数を表現している。RightLeftEitherのサブクラスで、慣習的にRightには成功時の結果、Leftにはエラーオブジェクトを格納する。

def openFile(filename: String): Either[Exception, File] = {

...ファイルにアクセス...
if (...ファイルが開けたか...) Right(new File(...))
else Left(new Exception("..."))
}

この関数の呼び出し元は、match構文(他の言語のswitch構文に近い)を使いエラーの場合の処理と、成功した場合の処理を分けて行うことができる。

openFile(filename) match {

case Left(exception) => println(exception.toString) // エラー処理
case Right(file) => // 成功した場合の処理
}

TryEitherと似ているが、例外をthrowする関数をTry { ... }で囲むことで、例外をcatchし、戻り値に変換する働きがある。ScalaではJavaの資産を再利用することがあるが、Javaコードは例外を投げることがあるので、それを副作用のないScalaのスタイルに適合するためにこのTryがよく使われる。

def toInt(s: String): Try[Int] = Try {

Integer.parseInt(s) // この関数は例外をthrowすることがある
}
toInt("100") match {
case Failure(exception) => println(exception.toString) // パースエラーなどの処理
case Success(value) => value * 2 // 成功時の処理
}


まとめ


  • 例外を投げないことを推奨する言語としてGoとScalaの例を見た。

  • それらの言語では、例外を投げることをどう考えているかに触れた。Goは例外は複雑なコードにつながると考え、Scalaは副作用のない関数のために例外を避ける傾向があった。

  • それらの言語では、どういうエラーハンドリングをしているかを調べた。Goでは多値返却、ScalaではEitherなどを使うのを見た。