Делает ли перехват / выброс исключений нечистым в противном случае чистым методом?

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, я изменил конструктор на private и ввел статический фабричный метод с именем 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
У меня было прозрение из-за твоего комментария. В то время как я интеллектуально заинтригован чистотой, в практичности мне действительно нужны детерминизм и благоговейная прозрачность. Если я достигну чистоты, это просто дополнительный бонус. Спасибо. Кстати, меня подтолкнуло этот путь к тому, что Microsoft IntelliTest эффективен только против детерминированного кода.
Рок Энтони Джонсон
4
@RockAnthonyJohnson: Ну, весь код должен быть детерминированным или, по крайней мере, настолько детерминированным, насколько это возможно. Ссылочная прозрачность - это только один из способов получить этот детерминизм. Некоторые методы, такие как потоки, имеют тенденцию работать против этого детерминизма, поэтому существуют другие методы, такие как задачи, которые улучшают недетерминированные тенденции потоковой модели. Такие вещи, как генераторы случайных чисел и симуляторы Монте-Карло, также являются детерминированными, но по-другому основаны на вероятностях.
Роберт Харви
3
@zzzzBov: я не считаю фактическое, строгое определение «чистого» таким уж важным. Смотрите второй комментарий выше.
Роберт Харви
12

Ну да ... и нет.

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

{
    var ignoreThis = func(arg);
}

Если funcвсе чисто, оптимизатор может решить, что func(arg)можно заменить его результатом. Он еще не знает, каков результат, но он может сказать, что он не используется - поэтому он может просто сделать вывод, что это утверждение не имеет никакого эффекта, и удалить его.

Но если func(arg)случится бросить, это утверждение что-то делает - оно бросает исключение! Таким образом, оптимизатор не может удалить его - имеет значение, вызвана функция или нет.

Но...

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

Что, как говорится...

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

Если бы в C # был такой Errorкласс, как Java (и многие другие языки), я бы предложил бросить Errorвместо Exception. Это указывает на то, что пользователь функции сделал что-то не так (передал функцию, которая выбрасывает), и такие вещи разрешены в чистых функциях. Но C # не имеет Errorкласса, и исключения ошибок использования, кажется, происходят из Exception. Я предлагаю бросить ArgumentException, давая понять, что функция была вызвана с неверным аргументом.


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

Идан Арье
источник
1
+1 За предлагаемое вами использование ArgumentException и за рекомендацию не «выбрасывать исключения из чистых функций с намерением их перехвата».
Рок Энтони Джонсон
Ваш первый аргумент (пока "Но ...") кажется мне необоснованным. Исключение является частью контракта функции. Вы можете, концептуально, переписать любую функцию как (value, exception) = func(arg)и исключить возможность генерирования исключения (в некоторых языках и для некоторых типов исключений вам действительно нужно перечислить его с определением, таким же, как аргументы или возвращаемое значение). Теперь он будет таким же чистым, как и раньше (при условии, что он всегда возвращает aka, выдает исключение, учитывая тот же аргумент). Бросок исключения не является побочным эффектом, если вы используете эту интерпретацию.
AnoE
@AnoE Если бы вы написали funcтаким образом, блок, который я дал, мог бы быть оптимизирован, потому что вместо разматывания стека было бы закодировано выброшенное исключение ignoreThis, которое мы игнорируем. В отличие от исключений, которые разматывают стек, исключения, закодированные в типе возврата, не нарушают чистоту, и именно поэтому многие функциональные языки предпочитают использовать их.
Идан Арье
Точно ... Вы не проигнорировали бы исключение для этого воображаемого преобразования. Я предполагаю, что это все вопрос семантики / определения, я просто хотел указать, что существование или возникновение исключений не обязательно отменяет все понятия чистоты.
AnoE
1
@AnoE Но код, который я написал, будет игнорировать исключение для этого воображаемого преобразования. func(arg)вернет кортеж (<junk>, TheException())и, следовательно, ignoreThisбудет содержать кортеж (<junk>, TheException()), но поскольку мы никогда не используем ignoreThisисключение, это ни на что не повлияет.
Идан Арье
1

Одним из соображений является то, что блок try - catch не является проблемой. (Исходя из моих комментариев к вопросу выше).

Основная проблема заключается в том, что свойство North является вызовом ввода-вывода .

На этом этапе выполнения кода программе необходимо проверить ввод-вывод, предоставленный клиентским кодом. (Не имеет значения, что входные данные представлены в форме делегата или что входные данные уже были переданы номинально).

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


Мне непонятно, почему вы не хотите проверять звонок на Move [Check] Room? Согласно моему комментарию к вопросу:

Да. Что у вас при исполнении делегата может сработать только в первый раз или вернуть другую комнату при втором вызове? Вызов делегата можно считать / вызвать сам побочный эффект?

Как сказал выше Барт ван Инген Шенау:

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

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


Я бы предложил использовать метод Move [Check] Room. Это позволит вам разделить нечистые аспекты ввода / вывода в одном месте.

Аналогично ответу Роберта Харви :

Чтобы сделать его чистой функцией, верните фактический объект, который инкапсулирует ожидаемое значение из функции, и значение, указывающее на возможное состояние ошибки, например объект Maybe или объект Unit of Work.

Автор кода должен определить, как обрабатывать (возможное) исключение из входных данных. Затем метод может вернуть объект Room, или объект Null Room, или, возможно, выбросить исключение.

В этом случае это зависит от:

  • Домен Комнаты обрабатывает Исключения Комнаты как Нуль или Что-то Хуже.
  • Как уведомить клиентский код, звонящий на север, в Нулевую / Исключительную комнату. (Залог / переменная состояния / глобальное состояние / вернуть монаду / все, что угодно; некоторые более чистые, чем другие :)).
SH-
источник
-1

Хм ... я не чувствовал себя правильно по этому поводу. Я думаю, что вы пытаетесь сделать так, чтобы другие люди правильно выполняли свою работу, не зная, что они делают.

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

Что если передача функции недопустима для запуска во время создания объекта Room?

Исходя из кода, я не был уверен, что делает Север.

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

Что делать, если это долгосрочная функция? Вы не захотите блокировать вашу программу при создании объекта Room, что если вам нужно создать объект 1000 Room?

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

Если есть другой человек, который пишет потребителю, он / она может не знать «особого» условия создания объекта Room, что может вызвать исполнение, и они почесывают голову, пытаясь выяснить, почему.

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

Вы должны обрабатывать исключение только тогда, когда вы знаете, что и как с ним обращаться.

Я думаю, что ваш оригинальный код достаточно хорош, единственное, что вы, возможно, захотите обработать исключение на «Main» (или любом слое, который знает, как его обработать).

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

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

Любое использование исключений может быть переписано для использования явных проверок результатов ошибок. Исключением может быть только синтаксический сахар. Удобный синтаксический сахар не делает чистый код нечистым.

JacquesB
источник
1
Тот факт, что вы можете перезаписать нечистоту, не делает функцию чистой. Haskell является доказательством того, что использование нечистых функций может быть переписано чистыми функциями - он использует монады для кодирования любого нечистого поведения в возвращаемых типах чисто чистых функций. Существование монад IO не означает, что регулярный IO является чистым - почему существование монад исключений подразумевает, что регулярные исключения чисты?
Идан Арье
@IdanArye: я утверждаю, что в первую очередь нет примесей. Пример переписывания должен был только продемонстрировать это. Обычный ввод-вывод является нечистым, поскольку он вызывает наблюдаемые побочные эффекты или может возвращать разные значения с одним и тем же входом из-за некоторого внешнего ввода. Бросок исключения не делает ничего из этого.
JacquesB
Побочного эффекта свободного кода недостаточно. Ссылочная прозрачность также является требованием к чистоте функций.
Идан Арье
1
Детерминизма недостаточно для ссылочной прозрачности - побочные эффекты также могут быть детерминированными. Ссылочная прозрачность означает, что выражение можно заменить его результатами - поэтому независимо от того, сколько раз вы оцениваете ссылочные прозрачные выражения, в каком порядке и независимо от того, оцениваете ли вы их вообще, результат должен быть одинаковым. Если исключение выбрасывается детерминистически, мы хороши с критериями «независимо от того, сколько раз», но не с другими. Я спорил о критериях «есть или нет» в моем собственном ответе (продолжение ...)
Идан Арье,
1
(... продолжение), а что касается «в каком порядке» - если fи gявляются чистыми функциями, то f(x) + g(x)должны иметь одинаковый результат независимо от того, какой порядок вы оцениваете f(x)и g(x). Но если f(x)throws Fи g(x)throws G, то исключение, выброшенное из этого выражения, будет, Fесли f(x)вычисляется первым, а Gесли g(x)оценивается первым - это не то, как должны вести себя чистые функции! Когда исключение закодировано в возвращаемом типе, этот результат будет выглядеть примерно так, Error(F) + Error(G)независимо от порядка вычисления.
Идан Арье