Чи виловлювання / викидання винятків робить інакше чистий метод нечистим?


27

Наступні приклади коду забезпечують контекст мого запитання.

Клас Room ініціалізується делегатом. У першій реалізації класу Кімната немає охоронців проти делегатів, які кидають винятки. Такі винятки будуть переноситись до властивості North, де оцінюється делегат (зверніть увагу: метод Main () демонструє, як у клієнтському коді використовується екземпляр Room):

public sealed class Room
{
    private readonly Func<Room> north;

    public Room(Func<Room> north)
    {
        this.north = north;
    }

    public Room North
    {
        get
        {
            return this.north();
        }
    }

    public static void Main(string[] args)
    {
        Func<Room> evilDelegate = () => { throw new Exception(); };

        var kitchen = new Room(north: evilDelegate);

        var room = kitchen.North; //<----this will throw

    }
}

Оскільки я скоріше не працюю при створенні об'єкта, а не під час читання властивості North, я змінюю конструктор на приватний та ввожу статичний заводський метод на ім'я Create (). Цей метод ловить виняток, кинутий делегатом, і викидає виняток обгортки, маючи змістовне повідомлення про виключення:

public sealed class Room
{
    private readonly Func<Room> north;

    private Room(Func<Room> north)
    {
        this.north = north;
    }

    public Room North
    {
        get
        {
            return this.north();
        }
    }

    public static Room Create(Func<Room> north)
    {
        try
        {
            north?.Invoke();
        }
        catch (Exception e)
        {
            throw new Exception(
              message: "Initialized with an evil delegate!", innerException: e);
        }

        return new Room(north);
    }

    public static void Main(string[] args)
    {
        Func<Room> evilDelegate = () => { throw new Exception(); };

        var kitchen = Room.Create(north: evilDelegate); //<----this will throw

        var room = kitchen.North;
    }
}

Чи робить блок "try-catch" чистим метод Create ()?


1
Яка користь від функції створення кімнати; якщо ви користуєтесь делегатом, навіщо називати його до того, як клієнт зателефонував "Північ"?
SH–

1
@SH Якщо делегат кидає виняток, я хочу дізнатися, коли створюється об'єкт Room. Я не хочу, щоб клієнт знаходив виняток під час використання, а скоріше при створенні. Метод Create - ідеальне місце для викриття злих делегатів.
Рок Ентоні Джонсон

3
@RockAnthonyJohnson, так. Що ви робите при виконанні делегата, можливо, працюєте лише перший раз або повертаєте іншу кімнату під час другого дзвінка? Виклик делегата може вважатися / викликати сам побічний ефект?
SH–

4
Функція, яка викликає нечисту функцію, є нечистою функцією, тому якщо делегат нечистий Createтакож є нечистою, тому що вона викликає її.
Ідан Ар'є

2
Ваша Createфункція не захищає вас від отримання винятку при отриманні майна. Якщо ваш делегат кине, в реальному житті дуже ймовірно, що він кинеться лише за деяких умов. Ймовірно, що умов для метання немає під час будівництва, але вони є при отриманні майна.
Барт ван Іґен Шенау

Відповіді:


26

Так. Це фактично нечиста функція. Це створює побічний ефект: виконання програми триває десь, крім місця, до якого очікується повернення функції.

Щоб зробити його чистою функцією, returnфактичний об'єкт, який інкапсулює очікуване значення від функції та значення, що вказує на можливий стан помилки, як Maybeоб'єкт або об'єкт «Одиниця роботи».


16
Пам'ятайте, що "чисте" - це лише визначення. Якщо вам потрібна функція, яка кидає, але в будь-який інший спосіб вона є прозоро прозорою, тоді це ви пишете.
Роберт Харві

1
У haskell принаймні будь-яка функція може кинутись, але ви повинні бути в IO, щоб зловити, чиста функція, яку іноді кидання зазвичай називають частковою
jk.

4
У мене виникло епіфанія через ваш коментар. Хоча мене інтелектуально заінтригує чистота, те, що мені насправді потрібно в практичності, - це детермінізм і благодатна прозорість. Якщо я досягну чистоти, це лише додатковий бонус. Дякую. FYI, те, що мене зводило на цей шлях, це те, що Microsoft IntelliTest ефективний лише проти детермінованого коду.
Рок Ентоні Джонсон

4
@RockAnthonyJohnson: Ну, весь код повинен бути детермінованим, або принаймні максимально детермінованим. Трансферна прозорість - лише один із способів отримати цей детермінізм. Деякі прийоми, як нитки, мають тенденцію працювати проти цього детермінізму, тому існують інші методи, такі як Завдання, які покращують недетерміновані тенденції моделі нитки. Такі речі, як генератори випадкових чисел та тренажери Монте-Карло, також детерміновані, але по-іншому ґрунтуються на ймовірності.
Роберт Харві

3
@zzzzBov: Я не вважаю власне суворим визначення "чистого" всього цього важливим. Дивіться другий коментар вище.
Роберт Харві

12

Ну так ... і ні.

Чиста функція повинна мати Референтну прозорість - тобто ви повинні мати можливість замінити будь-який можливий виклик чистої функції повернутим значенням, не змінюючи поведінку програми. * Ваша функція гарантовано завжди передає певні аргументи, тому немає зворотного значення для заміни виклику функції, тому замість цього давайте ігнорувати його. Розглянемо цей код:

{
    var ignoreThis = func(arg);
}

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

Але якщо func(arg)трапляється кидати, це твердження щось робить - це викидає виняток! Тож оптимізатор не може його видалити - має значення, виклик функції чи ні.

Але ...

На практиці це має значення дуже мало. Виняток - принаймні, на C # - щось виняткове. Ви не повинні використовувати його як частину регулярного контрольного потоку - ви повинні спробувати його зловити, і якщо ви щось зробите, виправляєте помилку, або поверніть те, що ви робили, або щоб якось все-таки виконати це. Якщо ваша програма не працює належним чином, оскільки код, який не вдався, був оптимізований, ви використовуєте винятки неправильно (якщо це не тестовий код, і коли ви створюєте тести, винятки не повинні оптимізуватися).

Це сумно...

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

Якби у C # був такий Errorклас, як Java (та багато інших мов), я б запропонував кинути Errorзамість an Exception. Це вказує, що користувач функції зробив щось не так (передав функцію, яка кидає), і такі речі дозволені в чистих функціях. Але C # не має Errorкласу, і винятки з помилок використання, як видається, походять Exception. Моя пропозиція - кинути ArgumentException, даючи зрозуміти, що функція викликалася поганим аргументом.


* Обчислювально. Функція Фібоначчі, реалізована за допомогою наївної рекурсії, потребуватиме довгого часу для великої кількості, і може вичерпати ресурси машини, але вони, оскільки за необмеженого часу та пам’яті, функція завжди поверне те саме значення і не матиме побічних ефектів (крім виділення пам'ять і зміна цієї пам'яті) - це все ще вважається чистим.


1
+1 Для запропонованого вами використання ArgumentException, а також для вашої рекомендації не "викидати винятки з чистих функцій з наміром, що вони будуть схоплені".
Рок Ентоні Джонсон

Перший ваш аргумент (доки "Але ...") здається мені невідомим. Виняток - частина договору функції. Ви можете, умовно, переписати будь-яку функцію як (value, exception) = func(arg)і видалити можливість викидання виключення (у деяких мовах та для деяких типів винятків вам потрібно перелічити її з визначенням, таким же як аргументи або повернене значення). Тепер воно було б таким же чистим, як і раніше (за умови, що він завжди повертає ака викидає виняток із тим самим аргументом). Закидання винятку не є побічним ефектом, якщо ви використовуєте цю інтерпретацію.
AnoE

@AnoE Якби ви написали funcтак, блок, який я дав, міг би бути оптимізований, тому що замість розмотування стека був би закодований викинутий виняток ignoreThis, який ми ігноруємо. На відміну від винятків, які розмотують стек, винятки, закодовані у типі повернення, не порушують чистоту - і саме тому багато функціональних мов вважають за краще їх використовувати.
Ідан Ар'є

Саме ... ви б не проігнорували виняток із цієї уявної трансформації. Напевно, це все трохи питання семантики / визначення, я просто хотів зазначити, що існування чи виникнення винятків не обов'язково анулює всі поняття чистоти.
AnoE

1
@AnoE Але код, який я написав , ігнорував би виняток для цього уявного перетворення. func(arg)поверне кортеж (<junk>, TheException()), і тому він ignoreThisбуде містити кортеж (<junk>, TheException()), але оскільки ми ніколи не використовуємо ignoreThisвиняток, це не вплине ні на що.
Ідан Ар’є

1

Одне враження полягає в тому, що блок пробного не є проблемою. (На основі моїх коментарів до питання вище).

Основна проблема полягає в тому, що властивість North - це дзвінок вводу-виводу .

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

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


Мені не зрозуміло, чому ви не хочете перевіряти дзвінок до Переміщення [Перевірити] номер? Відповідно до мого коментаря до питання:

Так. Що ви робите при виконанні делегата, можливо, працюєте лише перший раз або повертаєте іншу кімнату під час другого дзвінка? Виклик делегата може вважатися / викликати сам побічний ефект?

Як Барт ван Інген Шенау сказав вище,

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

Загалом, будь-який тип ледачого завантаження неявно відкладає помилки до цього моменту.


Я б запропонував скористатися методом Move [Check] Room. Це дозволить вам розділити нечисті аспекти вводу / виводу в одне місце.

Схожа на відповідь Роберта Харві :

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

Записувач кодів повинен визначити, як поводитися з (можливим) винятком із вхідних даних. Тоді метод може повернути об'єкт Room або об'єкт Null Room або, можливо, випустити виняток.

Саме цей момент залежить від:

  • Чи вважає домен Room винятки з кімнати нульовими або чимось гіршими.
  • Як повідомити код клієнта, що дзвонить на північ, у приміщенні Null / Exception. (Порука / Змінна статусу / Глобальний стан / Повернення монади / Що б там не було; Деякі більш чисті, ніж інші :)).

-1

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

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

Що робити, якщо передача функції не є допустимою для запуску під час створення об'єкта Room?

З коду я не міг бути впевнений, що робить Північ.

Скажімо, якщо функція - бронювати номер, і йому потрібен час і період бронювання, то ви отримаєте виняток, якщо не маєте готової інформації.

Що робити, якщо це тривала функція? Ви не хочете блокувати свою програму під час створення об'єкта Room, що робити, якщо вам потрібно створити об'єкт 1000 Room?

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

Якщо є інша людина, яка пише споживача, він / вона може не знати "особливої" умови створення об'єкта Кімната, що може спричинити вилучення, і вони почухають голову, намагаючись з'ясувати, чому.

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

Ви повинні обробляти виняток лише тоді, коли знаєте, що і як з цим поводитися.

Я думаю, що ваш оригінальний код є досить хорошим, єдине, що ви можете обробити виняток із "Main" (або будь-якого шару, який би знав, як з ним обробити).


1
Подивіться ще раз на 2-й приклад коду; Я ініціалізую новий виняток із нижчим винятком. Весь слід стека збережений.
Рок Ентоні Джонсон

На жаль Моя помилка.
користувач251630

-1

Я не погоджуюся з іншими відповідями і скажу « Ні» . Винятки не призводять до того, що інакше чиста функція стає нечистою.

Будь-яке використання винятків може бути переписане для використання явних перевірок на результати помилок. Винятки можна було б просто вважати синтаксичним цукром. Зручний синтаксичний цукор не робить чистий код нечистим.


1
Те, що ви можете переписати домішки далеко не робить функцію чистою. Haskell є доказом того, що використання нечистих функцій може бути переписано чистими функціями - він використовує монади для кодування будь-якої нечистої поведінки у повернених типах чисто чистих функцій. Наявність монад IO не означає, що регулярний IO чистий - чому існування монад винятків означає, що регулярні винятки є чистими?
Ідан Ар’є

@IdanArye: Я стверджую, що в першу чергу немає домішок. Приклад переписування був просто для того, щоб продемонструвати це. Регулярний IO нечистий, оскільки він викликає помітні побічні ефекти, або може повертати різні значення з однаковим входом через деякий зовнішній вхід. Кидати виняток не робить жодної з цих речей.
ЖакБ

Безкоштовного коду з побічними ефектами недостатньо. Референтна прозорість також є вимогою до чистоти функції.
Ідан Ар’є

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

1
(... продовження) і щодо "в якому порядку" - якщо fі gобидві чисті функції, то вони f(x) + g(x)повинні мати однаковий результат незалежно від порядку, який ви оцінюєте f(x)та g(x). Але якщо f(x)кидки Fі g(x)кидки G, то виняток, кинутий з цього виразу, був би, Fякщо f(x)він оцінюється спочатку і Gякщо g(x)оцінюється спочатку - це не так, як повинні поводитися чисті функції! Якщо виняток кодується у типі повернення, то такий результат закінчується як щось подібне Error(F) + Error(G)незалежно від порядку оцінки.
Ідан Ар'є
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.