Функціонування винятків у функціональному стилі


24

Мені сказали, що у функціональному програмуванні ніхто не повинен кидати та / або спостерігати за винятками. Натомість помилковий розрахунок слід оцінювати як нижнє значення. У Python (або інших мовах, які не повністю заохочують функціональне програмування) можна повертатись None(або інша альтернатива, яка трактується як нижнє значення, хоча Noneі не суворо відповідає визначенню), коли щось піде не так, щоб "залишатися чистим", але робити тому треба спостерігати помилку в першу чергу, тобто

def fn(*args):
    try:
        ... do something
    except SomeException:
        return None

Це порушує чистоту? І якщо так, то чи означає це, що в Python не можна суто обробляти помилки?

Оновлення

У своєму коментарі Ерік Ліпперт нагадав мені про інший спосіб поводження з винятками у ПП. Хоча я ніколи не бачив того, що робилося на Python на практиці, я грав із ним ще тоді, коли рік тому вивчав FP. Тут будь-яка optionalдекорована функція повертає 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

1
На ваше запитання вже відповіли, так що лише коментар: чи розумієте ви, чому, коли ваша функція кидає виняток, нахмурюється функціональне програмування? Це не довільна примха :)
Андрес Ф.

6
Існує ще одна альтернатива поверненню значення, що вказує на помилку. Пам'ятайте, що обробка виключень - це контрольний потік, і у нас є функціональні механізми перенаправлення потоків управління. Ви можете імітувати обробку винятків на функціональних мовах, написавши свій метод виконувати дві функції: продовження успіху та продовження помилки . Останнє, що робить ваша функція - викликати або продовження успіху, передачу "результату", або продовження помилки, передачу "винятку". Суть полягає в тому, що ви повинні писати свою програму таким чином, як всередині.
Ерік Ліпперт

3
Ні, якою була б держава в цьому випадку? Проблем є декілька, але ось кілька: 1 - є один можливий потік, який не кодується у типі, тобто ви не можете знати, чи буде функція кидати виняток, просто подивившись на її тип (якщо тільки ви не встановили прапорець винятки, звичайно, але я не знаю жодної мови, яка має лише їх). Ви ефективно працюєте "поза" типовою системою, 2- функціональні програмісти прагнуть записувати "загальні" функції, коли це можливо, тобто функції, які повертають значення для кожного вводу (забороняючи неприпинення). Виключення працюють проти цього.
Андрес Ф.

3
3- коли ви працюєте з загальними функціями, ви можете складати їх з іншими функціями та використовувати їх у функціях вищого порядку, не турбуючись про результати помилок, "не закодовані в тип", тобто винятки.
Андрес Ф.

1
@EliKorvigo Встановлення змінної вводить стан, за визначенням, повну зупинку. Вся мета змінних полягає в тому, що вони тримають стан. Це не має нічого спільного з винятками. Спостереження за поверненням функції та встановлення значення змінної на основі спостереження вводить стан в оцінку, чи не так?
користувач253751

Відповіді:


20

Перш за все, давайте розберемо деякі помилки. Немає "нижнього значення". Нижній тип визначається як тип, який є підтипом усіх інших типів у мові. З цього можна довести (хоча б у будь-якій цікавій системі типів), що нижній тип не має значень - він порожній . Тож немає такого поняття, як нижнє значення.

Чому корисний нижній тип? Ну, знаючи, що це порожньо, давайте зробимо деякі вирахування щодо поведінки програми. Наприклад, якщо у нас є функція:

def do_thing(a: int) -> Bottom: ...

ми знаємо, що do_thingніколи не може повернутися, оскільки він повинен був би повернути значення типу Bottom. Таким чином, є лише дві можливості:

  1. do_thing не зупиняється
  2. do_thing кидає виняток (мовами з механізмом виключення)

Зауважте, що я створив тип, Bottomякий насправді не існує мовою Python. Noneє неправильним; це насправді одиничне значення , єдине значення типу одиниці , яке викликається NoneTypeв Python (зробіть type(None)для підтвердження для себе).

Тепер ще одна помилка - це те, що функціональні мови не мають винятку. Це теж не вірно. Наприклад, SML має дуже приємний механізм виключення. Однак у SML винятки використовуються набагато щадніше, ніж, наприклад, у Python. Як ви вже говорили, загальним способом вказувати на якийсь збій у функціональних мовах є повернення Optionтипу. Наприклад, ми створили б функцію безпечного поділу наступним чином:

def safe_div(num: int, den: int) -> Option[int]:
  return Some(num/den) if den != 0 else None

На жаль, оскільки Python насправді не має типів суми, це не є життєздатним підходом. Ви можете повернутися Noneяк тип варіанту для бідного чоловіка, щоб позначити невдачу, але це насправді не краще, ніж повернення Null. Немає безпеки безпеки типу.

Тож я б радив дотримуватися мовних умов у цьому випадку. Python використовує винятки ідіоматично для управління потоком управління (що є поганим дизайном, IMO, але все-таки це стандарт), тому, якщо ви не працюєте лише з написаним вами кодом, я рекомендую дотримуватися стандартної практики. Чи це "чисте" чи ні, не має значення.


Під "нижнім значенням" я фактично мав на увазі "тип нижнього", тому я написав, що Noneне відповідає визначенню. У будь-якому випадку, дякую, що мене виправили. Ви не думаєте, що використовувати виключення лише для повного припинення виконання або для повернення необов'язкового значення нормально з принципами Python? Я маю на увазі, чому погано утримуватися від винятку для складного контролю?
Елі Корвіго

@EliKorvigo Це те, що я більш-менш сказала, правда? Виняток становлять ідіоматичні Python.
садівник

1
Наприклад, я заважаю студентам, які здобувають ступінь нижчого класу, використовувати try/except/finallyяк іншу альтернативу if/else, тобто try: var = expession1; except ...: var = expression 2; except ...: var = expression 3..., хоча це звичайна річ робити будь-якою імперативною мовою (btw, я також сильно заважаю використовувати if/elseблоки для цього). Ви маєте на увазі, що я нерозумний і повинен дозволити такі зразки, оскільки "це Python"?
Елі Корвіго

@EliKorvigo Я згоден з тобою взагалі (btw, ти професор?). try... catch...повинен НЕ бути використаний для управління потоком. Чомусь саме спільнота Python вирішила все робити. Наприклад, safe_divзазвичай писалася функція, про яку я писав вище try: result = num / div: except ArithmeticError: result = None. Тож, якщо ви навчаєте їх загальним принципам програмної інженерії, вам це обов'язково слід відмовити. if ... else...- це також кодовий запах, але це занадто довго, щоб займатись тут.
садівник

2
Існує "нижнє значення" (воно використовується, наприклад, у розмові про семантику Haskell), і воно мало стосується нижнього типу. Тож це насправді не помилкове уявлення про ОП, а лише про інше.
Бен

11

Оскільки протягом останніх кількох днів був такий великий інтерес до чистоти, чому б ми не вивчили, як виглядає чиста функція.

Чиста функція:

  • Є референтно-прозорим; тобто для даного входу він завжди буде мати однаковий вихід.

  • Не викликає побічних ефектів; він не змінює входи, виходи чи що-небудь інше у своєму зовнішньому середовищі. Це створює лише повернене значення.

Тож запитайте себе. Чи виконує ваша функція що-небудь, крім прийняття вводу та повернення результату?


3
Отже, не має значення, наскільки потворно написано всередині, поки воно поводиться функціонально?
Елі Корвіго

8
Правильно, якщо вас просто цікавить чистота. Звичайно, можна врахувати й інші речі.
Роберт Харві

3
Щоб грати в захисника чортів, я б стверджував, що кидати виняток - це лише форма виводу, а функції, які кидають, є чистими. Це проблема?
Бергі

1
@Bergi Я не знаю, що ти граєш на захисника диявола, оскільки саме ця відповідь передбачає :) Проблема полягає в тому, що крім чистоти є й інші міркування. Якщо ви допускаєте неперевірені винятки (які за визначенням не є частиною підпису функції), то тип повернення кожної функції фактично стає T + { Exception }(де Tявно оголошений тип повернення), що є проблематичним. Ви не можете знати, чи буде функція кидати виняток, не дивлячись на її вихідний код, що також робить записування функцій вищого порядку проблематичним.
Андрес Ф.

2
@Begri при киданні може бути імовірно чистим, IMO, використовуючи винятки, більше, ніж просто ускладнює тип повернення кожної функції. Це шкодить композиційності функцій. Розглянемо реалізацію, map : (A->B)->List A ->List Bде A->Bможна помилитися. Якщо ми дозволимо f кинути виняток map f L , повернеться щось типу Exception + List<B>. Якщо замість цього ми дозволимо йому повернути optionalтип стилю map f L, замість цього повернеться Список <Додатково <B>> `. Цей другий варіант мені здається більш функціональним.
Майкл Андерсон

7

Семантика Haskell використовує "нижнє значення" для аналізу значення коду Haskell. Це не те, що ви дійсно використовуєте безпосередньо в програмуванні Haskell, і повернення None- це зовсім не та сама річ.

Нижнє значення - це значення, приписане семантикою Haskell будь-яким обчисленням, які не вдається оцінити нормально. Один із таких способів, як це можна зробити для обчислення Haskell, - це насправді, кинувши виняток! Отже, якщо ви намагалися використовувати цей стиль в Python, ви насправді просто повинні викидати винятки як звичайні.

Семантика Haskell використовує нижнє значення, оскільки Haskell лінивий; ви можете маніпулювати "значеннями", які повертаються за допомогою обчислень, які ще не працюють. Ви можете передавати їх функціям, вставляти їх у структури даних тощо. Таке неоцінене обчислення може кинути виняток або цикл назавжди, але якщо нам ніколи насправді не потрібно вивчати значення, то обчислення ніколи не будутьзапустіть і зіткнетесь з помилкою, і нашій загальній програмі вдасться зробити щось чітко визначене та закінчити. Отже, не бажаючи пояснювати, що означає код Haskell, вказуючи точну операційну поведінку програми під час виконання, ми натомість оголошуємо, що такі помилкові обчислення дають нижнє значення та пояснюємо, що це значення веде; в основному, що будь-який вираз, який повинен залежати від будь-яких властивостей найнижчого значення (крім існуючого), також призведе до нижнього значення.

Щоб залишатися "чистими", всі можливі способи отримання нижнього значення повинні розглядатися як рівнозначні. Це включає "нижнє значення", яке представляє нескінченний цикл. Оскільки немає можливості знати, що деякі нескінченні петлі насправді є нескінченними (вони можуть закінчитися, якщо просто запустити їх трохи довше), ви не можете вивчити жодне властивість нижнього значення. Ви не можете перевірити, чи є щось внизу, не можете порівняти його ні з чим іншим, не можете перетворити його в рядок, нічого. Все, що ви можете зробити з одним, це помістити його місця (параметри функцій, частина структури даних тощо) недоторканими та не вивченими.

У Python вже є такий вид дна; це "значення", яке ви отримуєте від виразу, яке кидає виняток, або не припиняється. Оскільки Python суворий, а не лінивий, такі "днища" не можна зберігати ніде і, можливо, залишати їх не вивченими. Тому немає потреби використовувати концепцію нижнього значення, щоб пояснити, як до обчислень, які не повертають значення, все ще можна трактувати так, ніби вони мають значення. Але також немає причин, щоб ви не могли подумати таким чином про винятки, якби хотіли.

Кидання винятків насправді вважається "чистим". Це ловити виключення , що чистота брейки - саме тому , що дозволяє перевіряти дещо - що про деякі нижніх значень, замість того , щоб розглядати їх всі взаємозамінні. У Haskell ви можете зафіксувати лише винятки в тому, IOщо дозволяє нечисті взаємозв'язки (так це зазвичай відбувається на досить зовнішньому шарі). Python не забезпечує чистоти, але ви все одно можете вирішити, які функції є частиною вашого "зовнішнього нечистого шару", а не чистих функцій, і дозволити собі лише ловити винятки там.

Повернення Noneзамість цього зовсім інше. None- не нижнє значення; ви можете перевірити, чи щось їй дорівнює, і абонент функції, що повернувся, Noneбуде продовжувати працювати, можливо, використовуючи Noneневідповідно.

Отже, якщо ви думали кинути виняток і хочете "повернутися внизу", щоб наслідувати підхід Haskell, ви просто нічого не робите. Нехай поширюється виняток. Саме це мають на увазі програмісти Haskell, коли вони говорять про функцію, що повертає нижнє значення.

Але це не те, що мають на увазі функціональні програмісти, коли вони кажуть уникати винятків. Функціональні програмісти віддають перевагу "загальним функціям". Вони завжди повертають дійсне найнижнє значення свого типу повернення для кожного можливого введення. Отже, будь-яка функція, яка може кинути виняток, не є повною функцією.

Причина, що нам подобається загальних функцій, полягає в тому, що їх набагато легше трактувати як "чорні скриньки", коли ми поєднуємо та маніпулюємо ними. Якщо у мене є загальна функція, що повертає щось типу A і загальну функцію, яка приймає щось типу A, я можу викликати другу на виході першого, нічого не знаючи про реалізацію будь-якого; Я знаю, що отримаю вагомий результат, незалежно від того, як оновлюється код будь-якої функції в майбутньому (доки зберігається їх сукупність і до тих пір, поки вони зберігають підпис одного типу). Таке розмежування проблем може бути надзвичайно потужним засобом для рефакторингу.

Це також дещо необхідно для надійних функцій вищого порядку (функцій, що управляють іншими функціями). Якщо я хочу написати код, який отримує абсолютно довільну функцію (з відомим інтерфейсом) як параметр, я маю трактувати це як чорне поле, тому що я не можу знати, які входи можуть викликати помилку. Якщо мені надано загальну функцію, жоден ввід не спричинить помилку. Аналогічно, абонент моєї функції вищого порядку не буде точно знати, які аргументи я використовую для виклику функції, яку вони мені передають (якщо вони не хочуть залежати від деталей моєї реалізації), тому передача загальної функції означає, що вони не повинні турбуватися про що я з цим роблю.

Отже, функціональний програміст, який радить вам уникати винятків, вважає за краще повернути значення, яке кодує або помилку, або дійсне значення, і вимагає, щоб для його використання ви готові обробити обидві можливості. Такі речі, як Eitherтипи чи Maybe/ Optionтипи, - це один із найпростіших підходів зробити це на більш сильно набраних мовах (як правило, використовуються в спеціальних синтаксисах або функціях вищого порядку, щоб допомогти склеїти речі, які потрібні Aз речами, які виробляють a Maybe<A>).

Функція, яка або повертає None(якщо сталася помилка), або якесь значення (якщо помилки не було) не дотримується жодної з перерахованих вище стратегій.

У Python з качкою введення стилю Either / Maybe використовується не дуже, замість того, щоб викидати винятки, з тестами, щоб перевірити, що код працює, а не довіряти функціям, щоб бути тотальним і автоматично комбінованим на основі їх типів. Python не має можливості застосовувати, щоб код використовував такі речі, як, можливо, типи належним чином; навіть якщо ви використовували його як предмет дисципліни, вам потрібні тести, щоб фактично здійснити ваш код, щоб підтвердити це. Тож виняток / нижчий підхід, ймовірно, більше підходить для чистого функціонального програмування в Python.


1
+1 Чудова відповідь! І дуже всебічно.
Андрес Ф.

6

Поки немає зовнішніх видимих ​​побічних ефектів і повернене значення залежить виключно від входів, тоді функція є чистою, навіть якщо вона робить деякі досить нечисті речі всередині.

Тож насправді залежить від того, що саме може спричинити викиди виключень. Якщо ви намагаєтесь відкрити файл із заданим шляхом, то ні, це не чисто, тому що файл може існувати, а може і не існувати, що може призвести до того, що значення повернення може змінюватися для одного входу.

З іншого боку, якщо ви намагаєтеся розібрати ціле число з заданого рядка і викинути виняток, якщо він не вдасться, це може бути чистим, доки жодні винятки не можуть випливати з вашої функції.

З іншого боку, функціональні мови, як правило, повертають тип одиниці, лише якщо існує лише одна можлива умова помилки. Якщо можливі кілька помилок, вони, як правило, повертають тип помилки з інформацією про помилку.


Ви не можете повернути нижній тип.
садівник

1
@gardenhead Ви маєте рацію, я думав про тип одиниці. Виправлено.
8bittree

У випадку з вашим першим прикладом, чи не є файлова система та які файли існують в ній просто одним із входів функції?
Vality

2
@Vaility Насправді у вас, ймовірно, є ручка або шлях до файлу. Якщо функція fчиста, ви очікуєте, f("/path/to/file")що завжди повернете одне і те ж значення. Що станеться, якщо фактичний файл буде видалений або змінений між двома викликами на f?
Андрес Ф.

2
@Vaility Функція може бути чистою, якби замість ручки до файлу ви передали їй фактичний вміст (тобто точний знімок байтів) файлу, і в цьому випадку ви можете гарантувати, що якщо входить той самий вміст, той самий вихід виходить. Але доступ до файлу не такий :)
Андрес Ф.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.