少し前に、Javaの代わりにScalaを使用し始めました。言語間の「変換」プロセスの一部は、Either
sの代わりにException
sを使用することを学ぶことでした。私はコーディングしてきましたしばらくの間この方法でしたが、最近私はそれが本当に良い方法であるかどうか疑問に思い始めました。
Either
がException
を上回る主な利点の1つは、パフォーマンスの向上です。 Exception
はスタックトレースを構築する必要があり、スローされています。ただし、私が理解している限りでは、Exception
をスローすることは要求の多い部分ではありませんが、スタックトレースの構築は重要です。
しかし、その後はいつでもscala.util.control.NoStackTrace
を使用してException
sを構築/継承することができます。さらに、Either
の左側が実際にException
である(パフォーマンスの向上が見られない)場合がたくさんあります。
Either
のもう1つの利点は、コンパイラの安全性です。 Scalaコンパイラは、(Javaのコンパイラとは異なり)処理されないException
sについて文句を言いません。しかし、私が間違っていない場合、この決定は、これで説明されているのと同じ理由で推論されますトピックなので、...
構文に関しては、Exception
- styleの方がずっとわかりやすいと思います。次のコードブロックを調べます(どちらも同じ機能を実現しています)。
Either
スタイル:
def compute(): Either[String, Int] = {
val aEither: Either[String, String] = if (someCondition) Right("good") else Left("bad")
val bEithers: Iterable[Either[String, Int]] = someSeq.map {
item => if (someCondition(item)) Right(item.toInt) else Left("bad")
}
for {
a <- aEither.right
bs <- reduce(bEithers).right
ignore <- validate(bs).right
} yield compute(a, bs)
}
def reduce[A,B](eithers: Iterable[Either[A,B]]): Either[A, Iterable[B]] = ??? // utility code
def validate(bs: Iterable[Int]): Either[String, Unit] = if (bs.sum > 22) Left("bad") else Right()
def compute(a: String, bs: Iterable[Int]): Int = ???
Exception
スタイル:
@throws(classOf[ComputationException])
def compute(): Int = {
val a = if (someCondition) "good" else throw new ComputationException("bad")
val bs = someSeq.map {
item => if (someCondition(item)) item.toInt else throw new ComputationException("bad")
}
if (bs.sum > 22) throw new ComputationException("bad")
compute(a, bs)
}
def compute(a: String, bs: Iterable[Int]): Int = ???
後者は私にはかなりすっきりしているように見え、障害を処理するコード(Either
またはtry-catch
のパターンマッチング)はどちらの場合もかなり明確です。
だから私の質問は-なぜEither
を(チェックした)Exception
よりも使用するのですか?
更新
答えを読んだ後、ジレンマの核心を提示できなかったのではないかと思いました。私の懸念はnotがtry-catch
がないことです。 Exception
でTry
を「キャッチ」するか、catch
を使用して例外をLeft
でラップすることができます。
Either
/Try
に関する私の主な問題は、途中の多くの点で失敗する可能性のあるコードを作成するときに発生します。これらのシナリオでは、障害が発生した場合、その障害をコード全体に伝播する必要があるため、コードがより扱いにくくなります(前述の例を参照)。
実際には、Exception
を使用してreturn
sなしでコードを壊す別の方法があります(実際にはScalaの別の「タブー」です)。コードは、Either
のアプローチよりも明確であり、Exception
スタイルよりも少しクリーンですが、キャッチされないException
sの恐れはありません。
def compute(): Either[String, Int] = {
val a = if (someCondition) "good" else return Left("bad")
val bs: Iterable[Int] = someSeq.map {
item => if (someCondition(item)) item.toInt else return Left("bad")
}
if (bs.sum > 22) return Left("bad")
val c = computeC(bs).rightOrReturn(return _)
Right(computeAll(a, bs, c))
}
def computeC(bs: Iterable[Int]): Either[String, Int] = ???
def computeAll(a: String, bs: Iterable[Int], c: Int): Int = ???
implicit class ConvertEither[L, R](either: Either[L, R]) {
def rightOrReturn(f: (Left[L, R]) => R): R = either match {
case Right(r) => r
case Left(l) => f(Left(l))
}
}
基本的にreturn Left
はthrow new Exception
を置き換えます。また、どちらかの暗黙的なメソッドrightOrReturn
は、スタックへの自動例外伝播の補足です。
Either
を命令型のtry-catch
ブロックとまったく同じように使用するだけであれば、もちろん、まったく同じことをするために別の構文のように見えます。 Eithers
は値です。同じ制限はありません。あなたはできる:
Eithers
の「catch」部分は、「try」部分のすぐ隣にある必要はありません。共通の機能で共有することもできます。これにより、懸念事項を適切に分離することがはるかに容易になります。Eithers
をデータ構造に格納します。それらをループしてエラーメッセージを統合し、失敗しなかった最初のメッセージを見つけたり、遅延評価したりすることができます。Eithers
で記述せざるを得ない定型文が嫌いですか?ヘルパー関数を記述して、特定のユースケースで作業しやすくすることができます。 try-catchとは異なり、言語デザイナーが考案したデフォルトのインターフェースだけにとらわれているわけではありません。ほとんどの使用例では、Try
のインターフェースが単純で、上記の利点のほとんどを備えており、通常はニーズも満たしています。 Option
は、成功または失敗のみに関心があり、失敗のケースに関するエラーメッセージなどの状態情報を必要としない場合、さらに単純になります。
私の経験では、Eithers
がTrys
以外の何か、おそらく検証エラーメッセージであり、Left
の値を集計する場合を除き、Exception
はLeft
よりも実際には優先されません。たとえば、いくつかの入力を処理していて、最初のエラーメッセージだけでなく、allエラーメッセージを収集したいとします。それらの状況は、少なくとも私にとっては、実際にはそれほど頻繁には起こりません。
Try-catchモデルの先を見ると、Eithers
を使用するほうがはるかに快適です。
更新:この質問はまだ注目されており、構文の質問を明確にするOPの更新を見たことがなかったので、さらに追加すると思いました。
例外の考え方を打ち破る例として、これは私が通常あなたのサンプルコードを書く方法です:
def compute(): Either[String, Int] = {
Right(someSeq)
.filterOrElse(someACondition, "someACondition failed")
.filterOrElse(_ forall someSeqCondition, "someSeqCondition failed")
.map(_ map {_.toInt})
.filterOrElse(_.sum <= 22, "Sum is greater than 22")
.map(compute("good",_))
}
ここでは、filterOrElse
のセマンティクスが、この関数で主に実行していることを完全にキャプチャしていることを確認できます。条件のチェックと、エラーメッセージと特定の失敗との関連付けです。これは非常に一般的な使用例であるため、filterOrElse
は標準ライブラリにありますが、含まれていない場合は作成できます。
これは、誰かが私のように正確に解決できない反例を思い付くポイントですが、私が作ろうとしているポイントは、例外とは異なり、Eithers
を使用する方法が1つだけではないということです。 allを調査して、Eithers
で使用できる関数を試してみると、ほとんどのエラー処理状況でコードを簡略化する方法があります。
2つは実際には非常に異なります。 Either
のセマンティクスは、失敗する可能性のある計算を表すだけではありません。
Either
を使用すると、2つの異なるタイプのいずれかを返すことができます。それでおしまい。
さて、これらの2つのタイプの1つmayまたはエラーを表さない可能性があり、もう1つのmayまたは成功を表さない可能性がありますが、それは多くの1つの可能なユースケースにすぎません。 Either
はそれよりもはるかに一般的です。名前を入力することを許可されているユーザーからの入力を読み取るメソッド、またはEither[String, Date]
を返す日付を使用することもできます。
または、C♯のラムダリテラルについて考えてみましょう。これらはFunc
またはExpression
に評価されます。つまり、無名関数に評価されるか、ASTに評価されます。 C♯では、これは「魔法のように」起こりますが、適切な型を指定したい場合は、Either<Func<T1, T2, …, R>, Expression<T1, T2, …, R>>
になります。ただし、2つのオプションのどちらもエラーではなく、どちらも「正常」でも「例外的」でもありません。これらは、同じ関数から返される可能性のある2つの異なる型です(この場合、同じリテラルに起因します)。 [注:実際、Func
は別の2つのケースに分割する必要があります。C♯にはUnit
型がないため、何も返さないものは同じように表現できないためです何かを返すものとして、つまりactual適切なタイプはEither<Either<Func<T1, T2, …, R>, Action<T1, T2, …>>, Expression<T1, T2, …, R>>
のようになります。]
ここで、Either
canを使用して、成功タイプと失敗タイプを表します。また、2つのタイプの一貫した順序に同意する場合は、biasEither
を使用して2つのタイプのいずれかを優先することもできます。これにより、モナディックチェーンを通じて障害を伝播できます。しかし、その場合、あなたはEither
にそれ自体が必ずしも必要としない特定のセマンティクスを与えています。
2つのタイプの1つがエラータイプに固定され、エラーを伝播するために片側にバイアスされるEither
は、通常Error
モナドと呼ばれ、表現するのに適しています潜在的に失敗する計算。 Scalaでは、この種の型はTry
と呼ばれます。
例外の使用をTry
と比較するよりもEither
と比較するほうが理にかなっています。
したがって、これがEither
がチェック済み例外に対して使用される理由1です。Either
は例外よりもはるかに一般的です。例外が適用されない場合にも使用できます。
ただし、例外の代わりにEither
(またはより可能性の高いTry
)を使用するという制限されたケースについて質問する可能性が最も高いです。その場合、まだいくつかの利点、または可能性のある別のアプローチがあります。
主な利点は、エラーの処理を延期できることです。戻り値を格納するだけで、それが成功であるかエラーであるかを確認することさえありません。 (後で処理します。)または、別の場所に渡します。 (他の人に心配させてください。)リストにまとめてください。 (それらすべてを一度に処理します。)モナディックチェーンを使用して、処理パイプラインに沿ってそれらを伝播できます。
例外のセマンティクスは言語に組み込まれています。たとえば、Scalaでは、例外によってスタックがほどかれます。それらを例外ハンドラーにバブリングさせると、ハンドラーは発生した場所に戻ってそれを修正することができません。 OTOH、それを巻き戻し、必要なコンテキストを破棄して修正する前に、スタックの下位にそれをキャッチした場合、コンポーネントのレベルが低すぎて、どのように続行するかについて十分な情報を得られない可能性があります。
これは、Smalltalkとは異なります。たとえば、例外はスタックを巻き戻さず、再開可能であり、例外をスタックの上位(おそらくデバッガと同じ位)でキャッチでき、エラーwaaaaayyyyyを修正しますダウンスタックにあり、そこから実行を再開します。または、例外の場合のように、発生、キャッチ、処理が2つ(発生とキャッチ処理)ではなく、3つの異なるものであるCommonLisp条件。
例外の代わりに実際のエラーの戻り値の型を使用すると、言語を変更しなくても、同様のことができるようになります。
そして、部屋には明らかな象がいます。例外は副作用です。副作用は悪です。注:これが必ずしも正しいと言っているのではありません。しかし、それは一部の人々が持っている見方であり、その場合、例外に対するエラータイプの利点は明白です。したがって、関数型プログラミングを実行する場合は、関数型のアプローチであるという唯一の理由でエラー型を使用できます。 (Haskellには例外があることに注意してください。また、誰もそれらを使いたくないということに注意してください。)
例外のいいところは、元のコードがすでに例外的な状況を分類していることです。 IllegalStateException
とBadParameterException
の違いは、より強い型付き言語で区別するのがはるかに簡単です。 「良い」と「悪い」を解析しようとしても、非常に強力なツール、つまりScalaコンパイラ)を利用できません。独自のThrowable
には、テキストメッセージ文字列に加えて、必要なだけの情報を含めることができます。
これは、Scala環境でのTry
の真のユーティリティです。Throwable
は、左のアイテムのラッパーとして機能し、作業を簡単にします。