Подтвердить что ты не робот

Почему возникает исключение побочного эффекта?

Согласно записи wikipedia для побочного эффекта повышение исключения представляет собой побочный эффект. Рассмотрим эту простую функцию python:

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

Вызов с помощью foo(None) всегда будет выполняться с исключением. Тот же вход, тот же выход. Это ссылочно прозрачно. Почему это не чистая функция?

4b9b3361

Ответ 1

Чистота нарушается только в том случае, если вы наблюдаете за исключением и принимаете решение, основанное на ней, которое изменяет поток управления. Фактически бросание значения исключений является ссылочно прозрачным - оно семантически эквивалентно неопределению или другим так называемым нижним значениям .

Если (чистая) функция не total, то она оценивает нижнее значение. Как вы кодируете нижнее значение до реализации - это может быть исключение; или без прерывания, или деления на ноль, или какой-либо другой сбой.

Рассмотрим чистую функцию:

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

Это не определено для всех входов. Для некоторых он оценивается снизу. Реализация кодирует это путем исключения исключения. Он должен быть семантически эквивалентен использованию типа Maybe или Option.

Теперь вы только нарушаете ссылочную прозрачность, когда видите нижнее значение, и принимаете на нем решения, - что может привести к детерминизму, поскольку может быть брошено много разных исключений, и вы не можете знать, какой из них. Поэтому по этой причине исключения catch находятся в монаде IO в Haskell, а генерация так называемых "неточных" исключений может быть выполнена чисто.

Поэтому просто неверно, что создание исключения является побочным эффектом как таковым. Это зависит от того, можете ли вы изменить поведение чистой функции на основе исключительной ценности - таким образом, нарушение ссылочной прозрачности - вот в чем проблема.

Ответ 2

В первой строке:

"В информатике функция или выражение, как говорят, имеют сторону эффект, если в дополнение к возврату значения он также изменяет некоторые состояния или имеет наблюдаемое взаимодействие с вызывающими функциями или внешний мир"

Состояние, которое оно изменяет, - это прекращение программы. Чтобы ответить на другой вопрос о том, почему он не является чистой функцией. Функция не является чистой, потому что бросание исключения завершает программу, поэтому имеет побочный эффект (ваша программа заканчивается).

Ответ 3

Ссылочная прозрачность также позволяет заменить вычисление (например, вызов функции) результатом самого вычисления, чего вы не можете сделать, если ваша функция вызывает исключение. Это потому, что исключения не участвуют в вычислении, но они должны быть уловками!

Ответ 4

Приведение исключения может быть либо чистым, либо не чистым, оно просто зависит от типа вызываемого исключения. Хорошим правилом большого пальца является то, что если исключение возбуждается кодом, оно чистое, но если оно создается аппаратным обеспечением, его обычно следует классифицировать как нечистое.

Это можно увидеть, посмотрев, что происходит, когда возникает исключение аппаратного обеспечения. Сначала возникает сигнал прерывания, после чего начинает выполняться обработчик прерываний. Проблема здесь в том, что обработчик прерываний не был аргументом вашей функции и не указан в вашей функции, а глобальная переменная. Всякий раз, когда глобальная переменная (aka state) читается или записывается, у вас больше нет чистой функции.

Сравните это с тем исключением, которое возникает в вашем коде: вы создаете значение Exception из набора известных, локально ограниченных аргументов или констант, и вы "бросаете" результат. Глобальных переменных не используется. Процесс выброса исключения - это, по сути, синтаксический сахар, предоставляемый вашим языком, он не вводит какое-либо недетерминированное или нечистое поведение. Как сказал Дон, "он должен быть семантически эквивалентен использованию типа Maybe или Option", что означает, что он также должен иметь все те же свойства, включая чистоту.

Когда я сказал, что повышение аппаратного исключения "обычно" классифицируется как побочный эффект, это не всегда так. Например, если компьютер, на котором работает ваш код, не вызывает прерывание, когда он вызывает исключение, но вместо этого накладывает специальное значение на стек, тогда он не классифицируется как нечистый. Я считаю, что ошибка NAN с плавающей точкой IEEE сбрасывается с использованием специального значения, а не прерывания, поэтому любые исключения, возникающие при выполнении математики с плавающей запятой, могут быть классифицированы как побочные эффекты бесплатно, поскольку значение не считывается из какого-либо глобального состояния, но константа, закодированная в FPU.

Взглянув на все требования к чистому фрагменту кода, исключения на основе кода и инструкцию throw синтаксического сахара отметят все поля, они не изменяют какое-либо состояние, они не имеют никакого взаимодействия с их вызывающими функциями или чем-либо вне их invocation, и они ссылочно прозрачны, но только после того, как компилятор имеет свой путь с вашим кодом.

Как и все чистые vs нечистые обсуждения, я исключил любое понятие времени выполнения или операций с памятью и работал в предположении, что любая функция, которая может быть реализована исключительно, реализована исключительно вне зависимости от ее фактической реализации. У меня также нет доказательств претензии IEEE с плавающей точкой NAN.

Ответ 5

Я понимаю, что это старый вопрос, но ответы здесь не совсем верны, ИМХО.

Ссылочная прозрачность относится к свойству выражения, если программа, к которой оно принадлежит, имеет точно такое же значение, если выражение заменяется его результатом. Должно быть ясно, что создание исключения нарушает ссылочную прозрачность и, следовательно, имеет побочные эффекты. Позвольте мне продемонстрировать, почему...

Я использую Scala для этого примера. Рассмотрим следующую функцию, которая принимает целочисленный аргумент i и добавляет к нему целочисленное значение j, а затем возвращает результат в виде целого числа. Если при добавлении двух значений возникает исключение, возвращается значение 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    

Хорошо, очевидно, произошло исключение. Там нет сюрпризов.

Но помните, выражение является ссылочно прозрачным, если мы можем заменить его результатом, таким образом, чтобы программа имела точно такой же смысл. В данном случае выражение, на котором мы фокусируемся, это 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 (есть эквиваленты на других языках), одно решение состоит в том, чтобы обернуть, возможно, ошибочные результаты в тип 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, была из-за какого-то плохого программирования с нашей стороны (логическая ошибка), то выбрасывание исключения - что привело бы к завершению Программа - может рассматриваться как отличный способ привлечь внимание к проблеме. Однако, если исключение вызвано обстоятельствами, которые находятся вне нашего непосредственного контроля (например, в результате переполнения целочисленного сложения), и мы должны быть в состоянии восстановиться после такого условия, то мы должны формализовать эту возможность как часть функции return значение, и использовать, но не бросать, исключение.