web-dev-qa-db-ja.com

Either over(checked)Exceptionを使用する理由

少し前に、Javaの代わりにScalaを使用し始めました。言語間の「変換」プロセスの一部は、Eithersの代わりにExceptionsを使用することを学ぶことでした。私はコーディングしてきましたしばらくの間この方法でしたが、最近私はそれが本当に良い方法であるかどうか疑問に思い始めました。

EitherExceptionを上回る主な利点の1つは、パフォーマンスの向上です。 Exceptionはスタックトレースを構築する必要があり、スローされています。ただし、私が理解している限りでは、Exceptionをスローすることは要求の多い部分ではありませんが、スタックトレースの構築は重要です。

しかし、その後はいつでもscala.util.control.NoStackTraceを使用してExceptionsを構築/継承することができます。さらに、Eitherの左側が実際にExceptionである(パフォーマンスの向上が見られない)場合がたくさんあります。

Eitherのもう1つの利点は、コンパイラの安全性です。 Scalaコンパイラは、(Javaのコンパイラとは異なり)処理されないExceptionsについて文句を言いません。しかし、私が間違っていない場合、この決定は、これで説明されているのと同じ理由で推論されますトピックなので、...

構文に関しては、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よりも使用するのですか?

更新

答えを読んだ後、ジレンマの核心を提示できなかったのではないかと思いました。私の懸念はnottry-catchがないことです。 ExceptionTryを「キャッチ」するか、catchを使用して例外をLeftでラップすることができます。

Either/Tryに関する私の主な問題は、途中の多くの点で失敗する可能性のあるコードを作成するときに発生します。これらのシナリオでは、障害が発生した場合、その障害をコード全体に伝播する必要があるため、コードがより扱いにくくなります(前述の例を参照)。

実際には、Exceptionを使用してreturnsなしでコードを壊す別の方法があります(実際にはScalaの別の「タブー」です)。コードは、Eitherのアプローチよりも明確であり、Exceptionスタイルよりも少しクリーンですが、キャッチされないExceptionsの恐れはありません。

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 Leftthrow new Exceptionを置き換えます。また、どちらかの暗黙的なメソッドrightOrReturnは、スタックへの自動例外伝播の補足です。

17
Eyal Roth

Eitherを命令型のtry-catchブロックとまったく同じように使用するだけであれば、もちろん、まったく同じことをするために別の構文のように見えます。 Eithersです。同じ制限はありません。あなたはできる:

  • 試行ブロックを小さく連続させるという習慣をやめます。 Eithersの「catch」部分は、「try」部分のすぐ隣にある必要はありません。共通の機能で共有することもできます。これにより、懸念事項を適切に分離することがはるかに容易になります。
  • Eithersをデータ構造に格納します。それらをループしてエラーメッセージを統合し、失敗しなかった最初のメッセージを見つけたり、遅延評価したりすることができます。
  • それらを拡張してカスタマイズします。 Eithersで記述せざるを得ない定型文が嫌いですか?ヘルパー関数を記述して、特定のユースケースで作業しやすくすることができます。 try-catchとは異なり、言語デザイナーが考案したデフォルトのインターフェースだけにとらわれているわけではありません。

ほとんどの使用例では、Tryのインターフェースが単純で、上記の利点のほとんどを備えており、通常はニーズも満たしています。 Optionは、成功または失敗のみに関心があり、失敗のケースに関するエラーメッセージなどの状態情報を必要としない場合、さらに単純になります。

私の経験では、EithersTrys以外の何か、おそらく検証エラーメッセージであり、Leftの値を集計する場合を除き、ExceptionLeftよりも実際には優先されません。たとえば、いくつかの入力を処理していて、最初のエラーメッセージだけでなく、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で使用できる関数を試してみると、ほとんどのエラー処理状況でコードを簡略化する方法があります。

13
Karl Bielefeldt

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>>のようになります。]

ここで、Eithercanを使用して、成功タイプと失敗タイプを表します。また、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には例外があることに注意してください。また、誰もそれらを使いたくないということに注意してください。)

8
Jörg W Mittag

例外のいいところは、元のコードがすでに例外的な状況を分類していることです。 IllegalStateExceptionBadParameterExceptionの違いは、より強い型付き言語で区別するのがはるかに簡単です。 「良い」と「悪い」を解析しようとしても、非常に強力なツール、つまりScalaコンパイラ)を利用できません。独自のThrowableには、テキストメッセージ文字列に加えて、必要なだけの情報を含めることができます。

これは、Scala環境でのTryの真のユーティリティです。Throwableは、左のアイテムのラッパーとして機能し、作業を簡単にします。

0
BobDalgleish