Как реализовать побочные эффекты в чистом функциональном программировании?

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

Но есть одна вещь, которую я не понимаю: как бороться с побочными эффектами, ограничивая себя чистыми функциями.

Например, если я хочу вычислить сумму двух чисел, я могу написать чистую функцию (на JavaScript):

var add = function (first, second) {
  return first + second;
};

Совершенно никаких проблем. Но что, если я хочу вывести результат на консоль? Задача «вывести что-то на консоль» не является чистой по определению — но как я мог/должен с этим справиться на чистом функциональном языке программирования?


person Golo Roden    schedule 11.08.2013    source источник
comment
Существуют различные способы сделать это в чисто функциональных языках. На нечистых языках, я полагаю, у вас нет выбора...   -  person is7s    schedule 11.08.2013


Ответы (3)


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

Есть два подхода, которые я могу придумать навскидку. Существует по крайней мере третья, о которой я забыл.


Потоки ввода/вывода

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

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

Клей, который заставляет всю эту работу работать, — это волшебная машина, которая находится вне вашей программы, читает из потока «запросов» и помещает информацию в поток «ответов». Пока ваша программа чиста, эта волшебная машина — нет.

Выходной поток может выглядеть так:

[print('Hello, world! What is your name?'), input(), create_file('G:\testfile'), create_file('C:\testfile'), write_file(filehandle, 'John')]

и соответствующий входной поток будет

['John', IOException('There is no drive G:, could not create file!'), filehandle]

Видите, как input в out-stream привело к появлению 'John' во in-stream? Это принцип.

Монадический ввод-вывод

Монадический ввод-вывод — это то, что делает Haskell, и делает это очень хорошо. Вы можете представить это как построение гигантского дерева команд ввода-вывода с операторами, чтобы склеить их вместе, а затем ваша функция main возвращает это массивное выражение волшебной машине, которая находится вне вашей программы и выполняет команды и выполняет указанные операции. Эта волшебная машина нечиста, в то время как ваша программа построения выражения чиста.

Возможно, вы захотите представить, что это дерево команд выглядит примерно так:

main
  |
  +---- Cmd_Print('Hello, world! What is your name?')
  +---- Cmd_WriteFile
           |
           +---- Cmd_Input
           |
           +---+ return validHandle(IOResult_attempt, IOResult_safe)
               + Cmd_StoreResult Cmd_CreateFile('G:\testfile') IOResult_attempt
               + Cmd_StoreResult Cmd_CreateFile('C:\testfile') IOResult_safe

Первое, что он делает, это печатает приветствие. Следующее, что он делает, это то, что он хочет записать файл. Чтобы иметь возможность писать в файл, ему сначала нужно прочитать из ввода все, что он должен записать в файл. Затем предполагается, что у него есть дескриптор файла для записи. Он получает это от функции с именем validHandle, которая возвращает действительный дескриптор двух альтернатив. Таким образом, вы можете смешивать то, что выглядит как нечистый код, с тем, что выглядит как чистый код.


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

  • Реальный монадический ввод-вывод не похож на мой пример. Мой пример — одно из возможных объяснений того, как монадический ввод-вывод может выглядеть «под капотом», не нарушая чистоты.

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

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

    Это похоже на то, как вы не изучаете Java, глядя на дизассемблированный байт-код JVM.

  • Обязательно научиться использовать монадический ввод-вывод и потоковый ввод-вывод. Это классный опыт, и всегда хорошо иметь больше инструментов под своим поясом.

person kqr    schedule 11.08.2013
comment
и делает их нечистыми, взаимодействуя с окружающей средой --› и позволяет им направлять его во взаимодействии с окружающей средой. :) мне кажется. так что никаких загадок. Монады — это просто математика для программ (исходный код), Могги так говорит: мы отличаем объект A значений (типа A) от объекта TA вычислений (типа A), а в качестве обозначений программ (типа A) берем элементы TA.. - person Will Ness; 17.08.2013
comment
хм, вы на самом деле говорите именно это. :) - person Will Ness; 17.08.2013

Haskell, чистый функциональный язык, обрабатывает «нечистые» функции, используя «монады». Монада — это, по сути, шаблон, который упрощает цепочку вызовов функций с передачей продолжения. Концептуально функция печати в Haskell в основном принимает три параметра: строку для печати, состояние программы и остальную часть программы. Он вызывает остальную часть программы, передавая новое состояние программы, в котором строка находится на экране. Таким образом, ни одно состояние не было изменено.

Существует много подробных объяснений того, как работают монады, потому что по какой-то причине люди думают, что эту концепцию трудно понять: это не так. Вы можете найти многие из них, выполнив поиск в Интернете, я думаю, что этот мне нравится больше всего: http://blog.sigfpe.com/2006/08/you-could-have-invented-monads-и.html

person Joni    schedule 11.08.2013

ккр:

Существует по крайней мере третья, о которой я забыл.

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

Джони:

Концептуально функция печати в Haskell в основном принимает три параметра: строку для печати, состояние программы и остальную часть программы.

У вас было это и немного больше: нет необходимости в этом дополнительном параметре состояния; достаточно иметь остальную часть программы (это реализация, которая имеет дело с состоянием). См. раздел 3.2 (стр. 18) документа Как объявить императив Филипа Вадлера для деталей.

Незадолго до первой версии Haskell Ф. Уоррен Бертон предложил метод, основанный на псевдоданных (деревьях абстрактных значений) в Недетерминизм с ссылочной прозрачностью в языках функционального программирования. Позже Леннарт Аугустссон написал о другом подходе в меморандуме Функциональный ввод-вывод с использованием системных токенов.

Техника Бертона интересна тем, что в отличие от передачи состояния и родственных подходов, определения не должны возвращать измененные узлы с результатом или в качестве результата. Напротив, монадический подход, как правило, довольно агрессивен — если другие определения, основанные на результатах, основанных на эффектах, не могут быть легко отменены. в новый монадический контекст, их нужно будет изменить:

Расхвалив монады до краев, позвольте мне высказать одно замечание. Монады имеют тенденцию быть предложением «все или ничего». Если вы обнаружите, что вам нужно взаимодействие глубоко внутри вашей программы, вы должны переписать этот сегмент, чтобы использовать монаду. [...]

(из стр. 29 статьи Уодлера.)

Но аспекты метода Бертона привлекли такое же внимание — в О выразительности чисто функциональных систем ввода-вывода Пол Худак и Раман С. Сундареш отмечают, что дополнительные параметры и аргументы, которые для этого требуются, могут считаться неприятными.

ранний пример подхода Бертона можно найти в Состояние в Haskell, авторы Джон Лаунчбери и Саймон Пейтон Джонс.

person atravers    schedule 08.06.2020