web-dev-qa-db-ja.com

例外の発生が副作用であるのはなぜですか?

副作用 のウィキペディアのエントリによると、例外を発生させると副作用が発生します。この単純なpython関数:

_def foo(arg):
    if not arg:
        raise ValueError('arg cannot be None')
    else:
        return 10
_

foo(None)を使用して呼び出すと、常に例外が発生します。同じ入力、同じ出力。参照透過性です。なぜこれは純粋関数ではないのですか?

45
canadadry

純度に違反するのは、例外を観察し、それに基づいて制御フローを変更する決定を下した場合のみです。実際に例外値をスローすることは参照透過性です-それは意味的に非終了または他のいわゆる ボトム値。 と同等です

(純粋)関数が total でない場合、それは最下位の値に評価されます。一番下の値をどのようにエンコードするかは実装次第です-それは例外かもしれません。または非終了、またはゼロ除算、またはその他の障害。

純粋関数を考えてみましょう。

 f :: Int -> Int
 f 0 = 1
 f 1 = 2

これは、すべての入力に対して定義されているわけではありません。一部の人にとっては、それは底に評価されます。実装は、例外をスローすることによってこれをエンコードします。 MaybeまたはOption型を使用するのと意味的に同等である必要があります。

現在、参照透過性を破るのは、最下位の値を観察し、それに基づいて決定を下す場合のみです。これにより、多くの異なる例外として非決定論が導入される可能性があります。投げられるかもしれません、そしてあなたはどれを知ることができません。したがって、この理由で、例外のキャッチはHaskellのIOモナドにありますが、いわゆる "不正確な"例外 の生成は純粋に実行できます。

したがって、例外を発生させることがそれ自体の副作用であるというのは真実ではありません。問題は、例外的な値に基づいて純粋関数の動作を変更できるかどうか、つまり参照透過性を壊すことができるかどうかです。

33
Don Stewart

最初の行から:

「コンピュータサイエンスでは、関数または式は、値を返すだけでなく、ある状態を変更したり、呼び出し元の関数や外界との相互作用が観察できる場合、副作用があると言われています。」

変更する状態は、プログラムの終了です。なぜそれが純粋関数ではないのかについての他の質問に答えるため。例外をスローするとプログラムが終了するため、関数は純粋ではありません。そのため、副作用が発生します(プログラムが終了します)。

14
Woot4Moo

参照透過性は、計算(関数の呼び出しなど)を計算自体の結果に置き換える可能性もあります。これは、関数で例外が発生した場合には実行できません。これは、例外は計算に関与しませんが、キャッチする必要があるためです。

6

例外の発生は、純粋OR非純粋、発生する例外のタイプによって異なります。例外がコードによって発生する場合は、経験則として適切です。は純粋ですが、ハードウェアによって発生した場合は、通常、非純粋として分類する必要があります。

これは、ハードウェアによって例外が発生したときに何が発生するかを確認することで確認できます。最初に割り込み信号が発生し、次に割り込みハンドラが実行を開始します。ここでの問題は、割り込みハンドラーが関数の引数ではなく、関数で指定されたものではなく、グローバル変数であったことです。グローバル変数(別名状態)が読み取られたり書き込まれたりするときはいつでも、純粋関数はなくなります。

これをコードで発生する例外と比較してください。ローカルにスコープされた既知の引数または定数のセットからException値を作成し、その結果を「スロー」します。使用されるグローバル変数はありません。例外をスローするプロセスは、基本的に言語によって提供される構文糖衣構文であり、非決定論的または非純粋な動作を導入するものではありません。 Donが言ったように、「MaybeまたはOptionタイプを使用するのと意味的に同等である必要があります」。つまり、純度を含め、すべて同じプロパティを持っている必要があります。

ハードウェア例外の発生は「通常」副作用として分類されると私が言ったとき、必ずしもそうである必要はありません。たとえば、コードを実行しているコンピューターが例外を発生させたときに割り込みを呼び出さず、代わりに特別な値をスタックにプッシュする場合、それは非純粋として分類できません。 IEEE浮動小数点NANエラーは、割り込みではなく特別な値を使用してスローされると思います。したがって、浮動小数点演算の実行中に発生した例外は、値がグローバル状態から読み取られないため、副作用のないものとして分類できますが、 FPUにエンコードされた定数。

ピースコードが純粋であるためのすべての要件を見ると、コードベースの例外とスローステートメントの構文糖衣構文がすべてのボックスにチェックマークを付け、状態を変更せず、呼び出し元の関数や呼び出し以外のものとの相互作用がありません。それらは参照透過性ですが、コンパイラーがコードを処理した後のみです。

すべての純粋な議論と非純粋な議論のように、実行時間やメモリ操作の概念を除外し、純粋に実装できる関数はすべてIS実際に関係なく純粋に実装される)という仮定の下で動作しました実装。IEEEフローティングポイントNAN例外クレームの証拠もありません。

5
Ben Seidel

これは古い質問だと思いますが、ここでの答えは完全には正しくありません、私見。

参照透過性式が属するプログラムが、式がその結果に置き換えられた場合にまったく同じ意味を持つ場合、式が持つプロパティを参照します。例外をスローすると、参照透過性に違反し、その結果、副作用になることは明らかです。理由を説明しましょう...

この例ではScalaを使用しています。整数の引数iを取り、それに整数値jを追加して、結果を整数として返す次の関数について考えてみます。 2つの値の加算中に例外が発生すると、値0が返されます。残念ながら、jの値を計算すると、例外がスローされます(簡単にするために、jの初期化を置き換えました)。結果の例外を伴う式)。

def someCalculation(i: Int): Int = {
  val j: Int = throw new RuntimeException("Something went wrong...")
  try {
    i + j
  }
  catch {
    case e: Exception => 0 // Return 0 if we catch any exception.
  }
}

OK。少し馬鹿げていますが、非常に単純なケースでポイントを証明しようとしています。 ;-)

Scala REPLでこの関数を定義して呼び出し、何が得られるかを見てみましょう。

$ scala
Welcome to Scala 2.13.0 (OpenJDK 64-Bit Server VM, Java 11.0.4).
Type in expressions for evaluation. Or try :help.

scala> :paste
// Entering paste mode (ctrl-D to finish)

def someCalculation(i: Int): Int = {
  val j: Int = throw new RuntimeException("Something went wrong...")
  try {
    i + j
  }
  catch {
    case e: Exception => 0 // Return 0 if we catch any exception.
  }
}

// Exiting paste mode, now interpreting.

someCalculation: (i: Int)Int

scala> someCalculation(8)
Java.lang.RuntimeException: Something went wrong...
  at .someCalculation(<console>:2)
  ... 28 elided    

OK、明らかに例外が発生しました。そこに驚きはありません。

ただし、プログラムがまったく同じ意味を持つような結果に置き換えることができれば、式は参照透過性であることを忘れないでください。この場合、焦点を当てている式はjです。関数をリファクタリングして、jをその結果に置き換えましょう(スローされた例外の型は整数であると宣言する必要があります。これはjの型だからです)。

def someCalculation(i: Int): Int = {
  try {
    i + ((throw new RuntimeException("Something went wrong...")): Int)
  }
  catch {
    case e: Exception => 0 // Return 0 if we catch any exception.
  }
}

それでは、[〜#〜] repl [〜#〜]でそれを再評価しましょう:

scala> :paste
// Entering paste mode (ctrl-D to finish)

def someCalculation(i: Int): Int = {
  try {
    i + ((throw new RuntimeException("Something went wrong...")): Int)
  }
  catch {
    case e: Exception => 0 // Return 0 if we catch any exception.
  }
}

// Exiting paste mode, now interpreting.

someCalculation: (i: Int)Int

scala> someCalculation(8)
res1: Int = 0

まあ、おそらくあなたはそれが来るのを見たと思います:私たちはその頃に異なる結果を出しました。

jを計算し、それをtryブロックで使用しようとすると、プログラムは例外をスローします。ただし、jをブロック内の値に置き換えるだけでは、0になります。したがって、例外のスローは明らかに違反しています参照透過性

機能的の方法でどのように進めるべきですか?例外をスローしないことによって。 Scala(他の言語にも同等のものがあります)では、1つの解決策は、失敗する可能性のある結果をTry[T]タイプでラップすることです。成功した場合、結果はSuccess[T]でラップされます。成功した結果;障害が発生した場合、結果は関連する例外を含むFailure[Throwable]になります。どちらの式もTry[T]のサブタイプです。

import scala.util.{Failure, Try}

def someCalculation(i: Int): Try[Int] = {
  val j: Try[Int] = Failure(new RuntimeException("Something went wrong..."))

  // Honoring the initial function, if adding i and j results in an exception, the
  // result is 0, wrapped in a Success. But if we get an error calculating j, then we
  // pass the failure back.
  j.map {validJ =>
    try {
      i + validJ
    }
    catch {
      case e: Exception => 0 // Result of exception when adding i and a valid j.
    }
  }
}

注:例外は引き続き使用します。例外はスローしません。

[〜#〜] repl [〜#〜]でこれを試してみましょう:

scala> :paste
// Entering paste mode (ctrl-D to finish)

import scala.util.{Failure, Try}

def someCalculation(i: Int): Try[Int] = {
  val j: Try[Int] = Failure(new RuntimeException("Something went wrong..."))

  // Honoring the initial function, if adding i and j results in an exception, the
  // result is 0, wrapped in a Success. But if we get an error calculating j, then we
  // pass the failure back.
  j.map {validJ =>
    try {
      i + validJ
    }
    catch {
      case e: Exception => 0 // Result of exception when adding i and a valid j.
    }
  }
}

// Exiting paste mode, now interpreting.

import scala.util.{Failure, Try}
someCalculation: (i: Int)scala.util.Try[Int]

scala> someCalculation(8)
res2: scala.util.Try[Int] = Failure(Java.lang.RuntimeException: Something went wrong...)

今回は、jをその値に置き換えると、まったく同じ結果が得られます。これはすべての場合に当てはまります。

ただし、これには別の見方があります。jの値を計算するときに例外がスローされた理由が、私たちの側のプログラミングの誤り(論理エラー)によるものである場合、例外をスローすると(プログラムが終了することになります)、問題を私たちの注意を引くための優れた方法と見なすことができます。ただし、例外が私たちの直接の制御を超えた状況(整数加算のオーバーフローの結果など)にあり、そのような状態から回復できるはずである場合は、関数の戻り値の一部としてその可能性を形式化する必要があります値を設定し、例外を使用しますが、スローはしません。

4
Mike Allen