Мне сказали, что в функциональном программировании нельзя бросать и / или наблюдать исключения. Вместо этого ошибочный расчет должен оцениваться как нижнее значение. В Python (или других языках, которые не полностью поддерживают функциональное программирование) можно возвращать None
(или другую альтернативу, рассматриваемую как нижнее значение, хотя None
и не строго соблюдающее определение), когда что-то идет не так, чтобы «остаться чистым», но сделать поэтому нужно наблюдать ошибку в первую очередь, т.е.
def fn(*args):
try:
... do something
except SomeException:
return None
Это нарушает чистоту? И если так, значит ли это, что невозможно обработать ошибки исключительно в Python?
Обновить
В своем комментарии Эрик Липперт напомнил мне еще один способ обработки исключений в FP. Хотя я никогда не видел такого на практике в Python, я играл с ним еще тогда, когда изучал FP год назад. Здесь любая optional
-decorated функция возвращает Optional
значения, которые могут быть пустыми, для обычных выходных данных, а также для указанного списка исключений (неопределенные исключения все еще могут прервать выполнение). Carry
создает отложенную оценку, в которой каждый шаг (отложенный вызов функции) либо получает непустой Optional
вывод с предыдущего шага и просто передает его, либо иным образом оценивает себя, передавая новый Optional
. В конце концов, окончательное значение либо нормальное, либо Empty
. Здесь try/except
блок скрыт за декоратором, поэтому указанные исключения могут рассматриваться как часть сигнатуры возвращаемого типа.
class Empty:
def __repr__(self):
return "Empty"
class Optional:
def __init__(self, value=Empty):
self._value = value
@property
def value(self):
return Empty if self.isempty else self._value
@property
def isempty(self):
return isinstance(self._value, BaseException) or self._value is Empty
def __bool__(self):
raise TypeError("Optional has no boolean value")
def optional(*exception_types):
def build_wrapper(func):
def wrapper(*args, **kwargs):
try:
return Optional(func(*args, **kwargs))
except exception_types as e:
return Optional(e)
wrapper.__isoptional__ = True
return wrapper
return build_wrapper
class Carry:
"""
>>> from functools import partial
>>> @optional(ArithmeticError)
... def rdiv(a, b):
... return b // a
>>> (Carry() >> (rdiv, 0) >> (rdiv, 0) >> partial(rdiv, 1))(1)
1
>>> (Carry() >> (rdiv, 0) >> (rdiv, 1))(1)
1
>>> (Carry() >> rdiv >> rdiv)(0, 1) is Empty
True
"""
def __init__(self, steps=None):
self._steps = tuple(steps) if steps is not None else ()
def _add_step(self, step):
fn, *step_args = step if isinstance(step, Sequence) else (step, )
return type(self)(steps=self._steps + ((fn, step_args), ))
def __rshift__(self, step) -> "Carry":
return self._add_step(step)
def _evaluate(self, *args) -> Optional:
def caller(carried: Optional, step):
fn, step_args = step
return fn(*(*step_args, *args)) if carried.isempty else carried
return reduce(caller, self._steps, Optional())
def __call__(self, *args):
return self._evaluate(*args).value
источник
Ответы:
Прежде всего, давайте выясним некоторые заблуждения. Здесь нет «нижнего значения». Нижний тип определяется как тип, который является подтипом любого другого типа в языке. Отсюда можно доказать (по крайней мере, в любой интересной системе типов), что нижний тип не имеет значений - он пустой . Так что нет такой вещи, как нижнее значение.
Почему полезен нижний тип? Ну, зная, что оно пустое, давайте сделаем некоторые выводы о поведении программы. Например, если у нас есть функция:
мы знаем, что
do_thing
никогда не сможем вернуть, так как он должен будет вернуть значение типаBottom
. Таким образом, есть только две возможности:do_thing
не останавливаетсяdo_thing
выдает исключение (в языках с механизмом исключения)Обратите внимание, что я создал тип,
Bottom
который на самом деле не существует в языке Python.None
является неправильным; на самом деле это значение единицы , единственное значение типа единицы , которое вызываетсяNoneType
в Python (type(None)
для подтверждения сделай это сам).Теперь еще одно заблуждение заключается в том, что функциональные языки не имеют исключений. Это тоже не правда. SML, например, имеет очень хороший механизм исключений. Однако исключения используются в SML гораздо реже, чем, например, в Python. Как вы уже сказали, общий способ указать на какую-то ошибку в функциональных языках - это вернуть
Option
тип. Например, мы бы создали функцию безопасного деления следующим образом:К сожалению, поскольку Python на самом деле не имеет типов сумм, это нереальный подход. Вы можете вернуться
None
как тип опции бедного человека, чтобы обозначить неудачу, но это действительно не лучше, чем возвратNull
. Там нет типа безопасности.Поэтому я бы посоветовал придерживаться языковых соглашений в этом случае. Python идиоматически использует исключения для управления потоком управления (что является плохим дизайном, IMO, но, тем не менее, стандартным), поэтому, если вы не работаете только с написанным вами кодом, я бы рекомендовал следовать стандартной практике. Является ли это «чистым» или нет, не имеет значения.
источник
None
он не соответствует определению. В любом случае, спасибо, что поправили меня. Не думаете ли вы, что использование исключения только для полной остановки выполнения или для возврата необязательного значения - это нормально с принципами Python? Я имею в виду, почему это плохо, чтобы не использовать исключения для сложного управления?try/except/finally
как альтернативуif/else
, то естьtry: var = expession1; except ...: var = expression 2; except ...: var = expression 3...
, хотя это обычное дело на любом императивном языке (кстати, я настоятельно не рекомендую использоватьif/else
блоки и для этого). Ты имеешь в виду, что я безрассуден и должен допускать такие паттерны, потому что "это Python"?try... catch...
должен не быть использован для управления потоком. По какой-то причине, именно так сообщество Python и решило что-то сделать. Например,safe_div
функция, которую я написал выше, обычно пишетсяtry: result = num / div: except ArithmeticError: result = None
. Так что, если вы учите их общим принципам разработки программного обеспечения, вам определенно следует препятствовать этому.if ... else...
это также запах кода, но это слишком долго, чтобы вдаваться в подробности.Поскольку за последние несколько дней был очень большой интерес к чистоте, почему бы нам не посмотреть, как выглядит чистая функция.
Чистая функция:
Ссылочно-прозрачный; то есть для данного ввода он всегда будет производить один и тот же вывод.
Не вызывает побочных эффектов; он не меняет входы, выходы или что-либо еще во внешней среде. Это только производит возвращаемое значение.
Так что спросите себя. Ваша функция делает что-нибудь, кроме принятия ввода и возврата вывода?
источник
T + { Exception }
(гдеT
явно объявленный тип возвращаемого значения), что является проблематичным. Вы не можете знать, вызовет ли функция исключение, не глядя на свой исходный код, что также затрудняет написание функций более высокого порядка.map : (A->B)->List A ->List B
гдеA->B
можно. Если мы позволим f сгенерировать исключение,map f L
он вернет что-то типаException + List<B>
. Если вместо этого мы позволим ему возвращатьoptional
тип стиля,map f L
то вместо этого вернем List <Optional <B >> `. Этот второй вариант кажется мне более функциональным.Семантика Haskell использует «нижнее значение» для анализа значения кода на Haskell. Это не то, что вы действительно используете непосредственно в программировании на Haskell, и возвращение
None
- это совсем не то же самое.Нижнее значение - это значение, приписываемое семантикой Haskell для любого вычисления, которое не может быть нормально оценено как значение. Одним из таких способов вычисления на Haskell может быть исключение! Так что, если вы пытались использовать этот стиль в Python, вы должны просто генерировать исключения как обычно.
Семантика Haskell использует нижнее значение, потому что Haskell ленив; Вы можете манипулировать «значениями», которые возвращаются вычислениями, которые еще не выполнялись. Вы можете передавать их в функции, вставлять их в структуры данных и т. Д. Такое неоцененное вычисление может вызвать исключение или цикл навсегда, но если нам на самом деле не нужно проверять значение, то вычисление никогда не будетзапустить и столкнуться с ошибкой, и наша общая программа может сделать что-то четко определенное и закончить. Поэтому, не желая объяснять, что означает код на Haskell, указав точное рабочее поведение программы во время выполнения, мы вместо этого объявляем, что такие ошибочные вычисления производят нижнее значение, и объясняем, как ведет себя это значение; в основном, что любое выражение, которое должно зависеть от каких-либо свойств нижнего значения (кроме существующего), также приведет к нижнему значению.
Чтобы оставаться «чистым», все возможные способы получения нижнего значения должны рассматриваться как эквивалентные. Это включает в себя «нижнее значение», которое представляет бесконечный цикл. Поскольку нет никакого способа узнать, что некоторые бесконечные циклы на самом деле бесконечны (они могут закончиться, если вы просто запустите их немного дольше), вы не можете исследовать какое-либо свойство нижнего значения. Вы не можете проверить, является ли что-то нижним, не можете сравнить это с чем-то еще, не можете преобразовать это в строку, ничего. Все, что вы можете сделать с одним, это поместить его (параметры функции, часть структуры данных и т. Д.) В нетронутое и неизученное состояние.
Python уже имеет такой вид дна; это «значение», которое вы получаете из выражения, которое выдает исключение или не завершается. Поскольку Python строгий, а не ленивый, такие «низы» не могут быть сохранены где-либо и потенциально могут остаться незамеченными. Таким образом, нет никакой реальной необходимости использовать концепцию нижнего значения, чтобы объяснить, как вычисления, которые не могут вернуть значение, все еще могут обрабатываться так, как если бы они имели значение. Но нет также причины, по которой вы не могли бы так думать об исключениях, если бы захотели.
Бросать исключения на самом деле считается «чистым». Он ловит исключения, которые нарушают чистоту - именно потому, что он позволяет вам что-то проверять относительно определенных нижних значений, а не обрабатывать их все взаимозаменяемо. В Haskell вы можете перехватывать только исключения,
IO
которые допускают нечистое взаимодействие (так, как правило, это происходит на довольно внешнем слое). Python не обеспечивает чистоту, но вы все равно можете сами решать, какие функции являются частью вашего «внешнего нечистого слоя», а не чистыми функциями, и позволяете себе ловить только исключения.Возвращение
None
вместо этого совершенно другое.None
это не нижнее значение; Вы можете проверить, равно ли что-либо этому, и вызывающая функция, которая возвратиласьNone
, продолжит работать, возможно, используяNone
не по назначению.Так что, если вы хотели выбросить исключение и хотите «вернуть дно», чтобы подражать подходу Хаскелла, вы просто ничего не делаете. Пусть исключение распространяется. Это именно то, что имеют в виду программисты на Haskell, когда говорят о функции, возвращающей нижнее значение.
Но это не то, что имеют в виду функциональные программисты, когда говорят избегать исключений. Функциональные программисты предпочитают «тотальные функции». Они всегда возвращают действительное не нижнее значение своего типа возврата для каждого возможного ввода. Поэтому любая функция, которая может генерировать исключение, не является полной функцией.
Причина, по которой нам нравятся тотальные функции, заключается в том, что их гораздо проще рассматривать как «черные ящики», когда мы их комбинируем и манипулируем. Если у меня есть функция total, возвращающая что-то типа A, и функция total, которая принимает что-то типа A, тогда я могу вызвать секунду на выходе первого, ничего не зная о реализации того или другого; Я знаю, что получу правильный результат, независимо от того, как будет обновлен код любой функции в будущем (при условии, что их совокупность сохраняется, и до тех пор, пока они сохраняют одну и ту же сигнатуру типа). Такое разделение интересов может быть чрезвычайно мощным средством рефакторинга.
Это также необходимо для надежных функций высшего порядка (функций, которые манипулируют другими функциями). Если я хочу написать код, который получает совершенно произвольную функцию (с известным интерфейсом) в качестве параметра, я должен рассматривать его как черный ящик, потому что у меня нет способа узнать, какие входы могут вызвать ошибку. Если мне дают общую функцию, то никакие данные не приведут к ошибке. Аналогично, вызывающая сторона моей функции более высокого порядка не будет точно знать, какие аргументы я использую для вызова функции, которую они передают мне (если они не хотят зависеть от моих деталей реализации), поэтому передача функции total означает, что им не нужно беспокоиться о что я с этим делаю.
Поэтому функциональный программист, который советует вам избегать исключений, предпочел бы, чтобы вы вместо этого возвращали значение, которое кодирует либо ошибку, либо действительное значение, и требует, чтобы для его использования вы были готовы использовать обе возможности. Такие вещи , как
Either
типы илиMaybe
/Option
типов являются одними из самых простых подходов , чтобы сделать это в более сильно типизированных языках (обычно используется со специальным синтаксисом или функций высшего порядка , чтобы помочь склеить вещи , которые нуждаются вA
с вещами , которые производятMaybe<A>
).Функция, которая либо возвращает
None
(если произошла ошибка), либо какое-либо значение (если ошибки не было) не следует ни одной из вышеуказанных стратегий.В Python с утиной типизацией стиль Either / Maybe используется не очень часто, вместо этого создаются исключения, с тестами для проверки работоспособности кода, а не для того, чтобы функции были целостными и автоматически комбинируемыми на основе их типов. В Python нет средств для принудительного применения этого кода, чтобы правильно использовать такие вещи, как типы Maybe; даже если вы используете его как дисциплину, вам нужны тесты, чтобы на самом деле использовать ваш код для проверки этого. Таким образом, подход исключение / основание, вероятно, больше подходит для чисто функционального программирования в Python.
источник
Пока нет видимых извне побочных эффектов и возвращаемое значение зависит исключительно от входных данных, тогда функция является чистой, даже если она делает некоторые довольно нечистые вещи внутри.
Так что это действительно зависит от того, что именно может вызвать исключения. Если вы пытаетесь открыть файл по заданному пути, то нет, это не чисто, поскольку файл может существовать или не существовать, что может привести к изменению возвращаемого значения для одного и того же ввода.
С другой стороны, если вы пытаетесь анализировать целое число из заданной строки и выдает исключение, если оно терпит неудачу, это может быть чисто, если только исключения не могут выпасть из вашей функции.
Напомним, что функциональные языки обычно возвращают тип модуля только в том случае, если существует только одно возможное условие ошибки. Если существует несколько возможных ошибок, они, как правило, возвращают тип ошибки с информацией об ошибке.
источник
f
чистая, выf("/path/to/file")
всегда будете возвращать одно и то же значение. Что произойдет, если фактический файл будет удален или изменен между двумя вызовамиf
?