いずれかのオーバー(チェック済み)例外を使用する理由


17

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

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

しかし、その後、常にを使用してExceptionsを構築/継承することができます。scala.util.control.NoStackTraceさらにEither、実際にはの左側が実際にException(パフォーマンスブーストを控える)であるケースがたくさんあります。

もう1つの利点Eitherは、コンパイラの安全性です。ScalaコンパイラExceptionは、(Javaのコンパイラとは異なり)ハンドルされていない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 = ???

後者は私にはかなりきれいに見え、障害を処理するコード(パターンマッチングon Eitherまたはtry-catch)はどちらの場合も非常に明確です。

だから私の質問は-なぜEither(チェック済み)を使用するのExceptionですか?

更新

答えを読んだ後、私は自分のジレンマの核心を示すことに失敗したかもしれないことに気付きました。私の懸念は;の欠如ではありませんtry-catchExceptionwithを「キャッチ」するTryか、catchを使用して例外をラップできますLeft

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

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

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 Leftreplaces throw new Exception、およびいずれかの暗黙的なメソッドrightOrReturnは、スタックの自動例外伝播の補足です。



@RobertHarveyは、ほとんどの記事で議論されているようTryです。Eithervs に関する部分は、メソッドの他のケースが「非例外的」である場合にsが使用されるべきであるとException述べているだけEitherです。まず、これは非常に曖昧な定義です。第二に、それは本当に構文上のペナルティに値するのでしょうか?Eitherつまり、構文のオーバーヘッドがなければ、s を使用してもかまいません。
エイアルロス

Either私にはモナドのように見えます。モナドが提供する機能的構成の利点が必要な場合に使用します。または、多分そうではありません
ロバートハーヴェイ

2
@RobertHarvey:技術的に言えば、Eitherそれ自体はモナドではありません。左側または右側への射影はモナドですがEither、それ自体はそうではありません。ただし、左または右のいずれかに「バイアス」することで、モナドにすることができます。ただし、その後、の両側に特定のセマンティックを付与しますEither。Scala Eitherはもともと偏りがありませんでしたが、最近では偏っていたため、最近では実際にはモナドですが、「モナド」は固有のプロパティでEitherはなく、偏った結果です。
ヨルグWミットタグ

@JörgWMittag:まあ、いい。つまり、モナドの良さのすべてにそれを使用でき、結果のコードがどれほど「クリーン」であるかに特に悩まされることはありません(実際、機能スタイルの継続コードは例外バージョンよりもクリーンだと思います)。
ロバートハーベイ

回答:


13

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

  • 試行ブロックを小さく連続的に保つ習慣を止めてください。の「キャッチ」部分はEithers、「トライ」部分のすぐ隣にある必要はありません。共通の機能で共有することもできます。これにより、懸念事項を適切に分離することがはるかに簡単になります。
  • ストアEithersデータ構造インチ それらをループしてエラーメッセージを統合し、失敗しなかった最初のメッセージを見つけたり、それらを遅延評価したりすることができます。
  • それらを拡張およびカスタマイズします。一緒に書くことを余儀なくされているボイラープレートが好きではありませんEithersか?ヘルパー関数を記述して、特定のユースケースでの作業を容易にすることができます。try-catchとは異なり、言語設計者が思いついたデフォルトのインターフェースだけにとどまることはありません。

ほとんどのユースケースに注意してください、a Tryはよりシンプルなインターフェースを持ち、上記の利点のほとんどを持ち、通常あなたのニーズも同様に満たします。Optionあなたが気にすべてを約成功または失敗であると失敗事例に関するエラーメッセージのような任意の状態情報を必要としない場合でも、簡単です。

私の経験でEithersは、が検証エラーメッセージである可能性があり、値を集計する必要があるTrys場合を除き、実際に優先されることはありません。たとえば、いくつかの入力を処理していて、最初のエラーメッセージだけでなく、すべてのエラーメッセージを収集たい場合です。これらの状況は、少なくとも私にとっては、実際にはあまり頻繁に発生しません。LeftExceptionLeft

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つだけではないということです。使用可能なすべての関数を調べEithers、実験に開放されている場合、ほとんどのエラー処理状況でコードを簡素化する方法があります。


1. tryとを分離できることcatchは公平な議論ですが、これは簡単に達成できませんTryか?2. メカニズムEitherと並行して、データ構造にsを保存できますthrows。それは単に障害を処理するための余分な層ではありませんか?3. Exceptions を確実に拡張できます。確かに、try-catch構造を変更することはできませんが、障害の処理は非常に一般的であるため、言語にそのためのボイラープレートを提供してほしいです(これは使用を強制されません)。それは、for-comprehensionがモナド演算の定型的なものだからだと言っているようなものです。
エイアルロス

Eithersを拡張できるが、明らかに拡張できない(あなたはsealed trait両方とも実装が2つしかない)ことをあなたが言ったことに気付いたfinal。それについて詳しく説明していただけますか?私はあなたが拡張の文字通りの意味を意味していなかったと仮定しています。
エイアルロス

1
プログラミングキーワードではなく、英語の単語のように拡張することを意味しました。一般的なパターンを処理する関数を作成して機能を追加します。
カールビーレフェルト

8

2つは実際には非常に異なっています。Eitherのセマンティクスは、失敗する可能性のある計算を表すためだけのものではありません。

Either2つの異なるタイプのいずれかを返すことができます。それでおしまい。

さて、これら二つのタイプの一つがあり、またはエラーやその他表していない可能性が5月にか成功を表していないかもしれないが、それは、多くの唯一の可能なユースケースです。Eitherそれよりもはるかに一般的です。名前を入力するか、日付を返すことを許可されているユーザーからの入力を読み取るメソッドを使用することもできEither[String, Date]ます。

または、C♯のラムダリテラルについて考えてみてください。これらFuncExpression、またはに評価されます。つまり、匿名関数に評価されるか、ASTに評価されます。C♯では、これは「魔法のように」起こりますが、適切なタイプを指定したい場合はになりますEither<Func<T1, T2, …, R>, Expression<T1, T2, …, R>>。ただし、2つのオプションはどちらもエラーではなく、どちらも「通常」または「例外」ではありません。これらは、同じ関数から返される可能性のある2つの異なるタイプです(この場合、同じリテラルに起因します)。[注:実際にFuncは、C♯にはUnit型がないため、別の2つのケースに分割する必要があります。したがって、何も返さないものは、何かを返すものと同じ方法で表すことができません。Either<Either<Func<T1, T2, …, R>, Action<T1, T2, …>>, Expression<T1, T2, …, R>>

現在、成功タイプと失敗タイプを表すために使用Either できます。また、2つのタイプの一貫した順序に同意する場合は、2つのタイプのいずれかを優先するようにバイアス Eitherをかけることもできます。これにより、モナドチェーンを通じて障害を伝播できます。しかし、その場合、あなたはEitherそれが必ずしもそれ自身で所有しているわけではないということに特定の意味を与えています。

Eitherエラーの種類に固定その2つのタイプのうちの1つを有する、エラーを伝播するために、1つの側に付勢され、通常呼ばれるErrorモナドを、潜在的に障害の計算を表現するためのより良い選択です。Scalaでは、この種の型はと呼ばれTryます。

これは、と例外の使用を比較するために、はるかに理にかなっているTryよりもEither

したがって、これがEitherチェック例外に対して使用される理由#1 Eitherです。例外よりもはるかに一般的です。例外が適用されない場合にも使用できます。

ただし、例外の代替として使用するEither(または、より可能性が高いTry)制限されたケースについて尋ねる可能性が最も高くなります。その場合、まだいくつかの利点、またはむしろ異なるアプローチが可能です。

主な利点は、エラーの処理を延期できることです。成功かエラーかを見なくても、戻り値を保存するだけです。(後で処理します。)または、別の場所に渡します。(他の人に心配させてください。)リストに集めてください。(すべてを一度に処理します。)モナド連鎖を使用して、処理パイプラインに沿ってそれらを伝播できます。

例外のセマンティクスは言語に組み込まれています。たとえば、Scalaでは、例外によってスタックが巻き戻されます。例外ハンドラーにバブルアップさせた場合、ハンドラーは発生した場所に戻って修正することはできません。OTOH、スタックを巻き戻して修正するために必要なコンテキストを破棄する前にスタック内でそれをキャッチした場合、処理方法について十分な情報に基づいた決定を下すにはレベルが低すぎるコンポーネントにいる可能性があります。

これは、例えば、Smalltalkのでは異なっている、例外はスタックをアンワインドし、再開可能であり、あなたは、(おそらくデバッガなどの高アップなど)スタック内の例外高を追いつくwaaaaayyyyyエラーを修正することができませんどこダウンでスタックし、そこから実行を再開します。または、例外の場合のように、レイズ、キャッチ、およびハンドリングが2つ(レイズおよびキャッチハンドリング)ではなく3つの異なるものであるCommonLisp条件。

例外の代わりに実際のエラー戻り型を使用すると、言語を変更することなく、同様のことができます。

そして、部屋には明らかな象がいます。例外は副作用です。副作用は悪です。注:これが必ずしも真実であると言っているわけではありません。しかし、それは一部の人々が持っている視点であり、その場合、例外に対するエラータイプの利点は明らかです。そのため、関数型プログラミングを行う場合、エラー型が関数型アプローチであるという唯一の理由でエラータイプを使用する場合があります。(Haskellには例外があることに注意してください。また、誰もそれらを使用することを好みません。)


答えが大好きですが、なぜ私はExceptions を忘れるべきなのかまだわかりません。Either素晴らしいツールのように見えますが、はい、私は特に障害の処理について尋ねています。そして、私はすべての強力なツールよりもはるかに特定のツールを自分のタスクに使用したいです。失敗の処理を延期することは公平な議論ですが、現時点でそれを処理したくない場合はTry、単にスローするメソッドで使用できExceptionます。スタックトレースの巻き戻しはEither/ Tryにも影響しませんか?間違って伝播すると、同じ間違いを繰り返す可能性があります。どちらの場合も、いつ障害を処理するかを知ることは簡単ではありません。
エイアルロス

そして、「純粋に機能的な」推論で議論することはできませんよね?
エイアルロス

0

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

これはTryScala環境の真のユーティリティであり、Throwable左のアイテムのラッパーとして機能し、生活を楽にします。


2
あなたの主張がわかりません。
エイアルロス

Either本当のモナド(?)ではないため、スタイルの問題があります。left例外的な条件に関する情報を適切な構造で適切に囲むと、値のbit意性が、より高い値から遠ざかります。そのためEither、ある場合に左側の文字列を返すの使用とtry/catch、別の場合に適切に入力された文字列を使用するのを比較することは適切でThrowables はありません。Throwables情報を提供することの価値、およびをTry処理する簡潔な方法によりThrowablesTry... Eitherまたはtry/catch
-BobDalgleish

私は実際には心配していませんtry-catch。質問で述べたように、後者は私にとってかなりきれいに見え、失敗を処理するコード(どちらかでのパターンマッチングまたはtry-catchのいずれか)はどちらの場合でもかなり明確です。
エイアルロス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.