Является ли польза от модели моно IO для обработки побочных эффектов чисто академической?

17

Извините за еще один вопрос о побочных эффектах FP +, но я не смог найти существующий, который вполне ответил на этот вопрос для меня.

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

Я также понял, что подход Хаскелла к этой проблеме, монада ввода / вывода, достигает этого, помещая в контейнер действия с состоянием для последующего выполнения, которые рассматриваются вне рамок самой программы.

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

Грубый пример входящего.

Если моя программа преобразует файл XML в файл JSON:

def main():
    xml_data = read_file('input.xml')  # impure
    json_data = convert(xml_data)  # pure
    write_file('output.json', json_data) # impure

Разве монада IO не подходит эффективно для этого:

steps = list(
    read_file,
    convert,
    write_file,
)

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

Или, другими словами, это все равно что писать:

def main():  # pure
    def inner():  # impure
        xml_data = read_file('input.xml')
        json_data = convert(xml_data)
        write_file('output.json', json_data)
    return inner

затем ожидать, что кто-то еще позвонит inner()и скажет, что ваша работа выполнена, потому что main()она чистая.

Вся программа в конечном итоге будет содержаться в монаде IO.

Когда код фактически выполняется , все после прочтения файла зависит от состояния этого файла, поэтому он по-прежнему будет страдать от тех же ошибок, связанных с состоянием, что и императивная реализация, так что вы действительно получили что-нибудь, как программист, который будет поддерживать это?

Я полностью ценю выгоду от сокращения и изоляции поведения с состоянием, именно поэтому я структурировал императивную версию так: собирать входные данные, делать чистые вещи, выплевывать выходные данные. Надеемся, что он convert()может быть абсолютно чистым и использовать преимущества кэшируемости, безопасности потоков и т. Д.

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

Есть ли какое-то дополнительное преимущество в борьбе с побочными эффектами, которые приносит паттерн ввода / вывода, чего мне не хватает?

Стю Кокс
источник
1
Вы должны посмотреть это видео . Чудеса монад наконец раскрываются, не прибегая к теории категорий или Хаскелу. Оказывается, что монады тривиально выражены в JavaScript и являются одним из ключевых факторов Ajax. Монады потрясающие. Это простые вещи, почти тривиально реализованные, с огромной способностью управлять сложностью. Но понять их на удивление сложно, и большинство людей, когда у них есть такой момент, кажется, теряют способность объяснять их другим.
Роберт Харви
Хорошее видео, спасибо. Я на самом деле узнал об этом от вступления JS до функционального программирования (потом прочитал еще миллион…). Хотя, посмотрев это, я вполне уверен, что мой вопрос относится именно к монаде ввода-вывода, которую Крок не рассматривает в этом видео.
Стю Кокс
Хм ... Разве AJAX не считается формой ввода / вывода?
Роберт Харви
1
Обратите внимание, что тип mainв программе на Haskell IO ()- действие IO. На самом деле это не функция; это ценность . Вся ваша программа представляет собой чистое значение, содержащее инструкции, которые сообщают языковой среде выполнения, что она должна делать. Все нечистое (фактически выполняющее действия ввода-вывода) выходит за рамки вашей программы.
Вайзард
В вашем примере монадическая часть - это когда вы берете результат одного вычисления ( read_file) и используете его в качестве аргумента для следующего ( write_file). Если бы у вас была только последовательность независимых действий, вам не понадобилась бы монада.
Лортабак

Ответы:

14

Вся программа в конечном итоге будет содержаться в монаде IO.

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

module Main

main :: IO ()
main = do
  xmlData <- readFile "input.xml"
  let jsonData = convert xmlData
  writeFile "output.json" jsonData

convert :: String -> String
convert xml = ...

Я думаю, что типичный подход Хаскеллера к этому будет convertчистой частью:

  1. Это, вероятно, основная часть этой программы, и гораздо более сложная, чем ее IOчасти;
  2. Можно рассуждать и проверять, не имея дело IOвообще.

Таким образом, они не видят это как convert«заключенное в себе» IO, а скорее как его изолированное от IO. От его типа, что бы ни convertделало, никогда не может зависеть от того, что происходит в IOдействии.

Когда код фактически выполняется, все после прочтения файла зависит от состояния этого файла, поэтому он по-прежнему будет страдать от тех же ошибок, связанных с состоянием, что и императивная реализация, так что вы действительно что-то получили, как программист, который будет поддерживать это?

Я бы сказал, что это разделяется на две вещи:

  1. Когда программа запускается, значение аргумента to convertзависит от состояния файла.
  2. Но то , что convertфункция делает , что не зависит от состояния файла. convertвсегда одна и та же функция , даже если она вызывается с разными аргументами в разных точках.

Это несколько абстрактный вопрос, но это действительно ключ к пониманию того, что имеют в виду Хаскеллерс, когда говорят об этом. Вы хотите написать convertтак, чтобы при любом действительном аргументе он приводил к правильному результату для этого аргумента. Когда вы смотрите на это так, то, что чтение файла является операцией с состоянием, не входит в уравнение; все, что имеет значение, - это то, что любой аргумент, который ему подают и откуда бы он ни исходил, convertдолжен обрабатывать его правильно. И тот факт, что чистота ограничивает то, что convertможно сделать с его входом, упрощает эти рассуждения.

Поэтому, если convertиз некоторых аргументов получаются неверные результаты и readFileпередаются им в качестве аргумента, мы не видим в этом ошибки, представленной состоянием . Это ошибка в чистой функции!

sacundim
источник
Я думаю, что это лучшее описание (хотя другие помогли мне прояснить ситуацию), спасибо.
Стю Кокс,
Стоит ли отмечать, что использование монад в Python может иметь меньшую выгоду, поскольку Python имеет только один (статический) тип и, следовательно, не дает никаких гарантий ни о чем?
JK.
7

Трудно точно понять, что вы имеете в виду под «чисто академическим», но я думаю, что ответ в основном «нет».

Как объяснено в « Борьбе с неуклюжим отрядом » Саймона Пейтона Джонса ( сильно рекомендуется к прочтению!), Монадический ввод-вывод предназначался для решения реальных проблем способом, используемым Haskell для обработки ввода-вывода. Прочитайте пример сервера с запросами и ответами, который я не буду здесь копировать; это очень поучительно.

Haskell, в отличие от Python, поощряет стиль «чистых» вычислений, который может быть реализован системой типов. Конечно, вы можете использовать самодисциплину при программировании на Python, чтобы соответствовать этому стилю, но как насчет модулей, которые вы не написали? Без особой помощи со стороны системы типов (и общих библиотек) монадический ввод-вывод, вероятно, менее полезен в Python. Философия языка не предназначена для строгого разделения между чистым и нечистым.

Обратите внимание, что это говорит больше о различных философиях Haskell и Python, чем о том, как академический монадический ввод / вывод. Я бы не использовал его для Python.

Еще одна вещь. Ты говоришь:

Вся программа в конечном итоге будет содержаться в монаде IO.

Это правда, что mainфункция Haskell «живет» IO, но реальным программам на Haskell рекомендуется не использовать ее, IOкогда она не нужна. Почти каждая функция, которую вы пишете, которая не нуждается в вводе / выводе, не должна иметь тип IO.

Так что я бы сказал, что в последнем примере вы получили это задом наперед: mainнечисто (потому что он читает и записывает файлы), но основные функции вроде бы convertчисты.

Андрес Ф.
источник
3

Почему IO нечист? Потому что он может возвращать разные значения в разное время. Существует зависимость от времени, которую необходимо учитывать, так или иначе. Это еще более важно с ленивой оценкой. Рассмотрим следующую программу:

main = do  
    putStrLn "Please enter your name"  
    name <- getLine
    putStrLn $ "Hello, " ++ name

Без монады ввода-вывода, почему первая подсказка когда-либо получит вывод? От этого ничего не зависит, поэтому ленивая оценка означает, что она никогда не будет востребована. Также нет ничего, что заставляло бы выводиться до чтения ввода. Что касается компьютера, то без монады ввода-вывода эти первые два выражения полностью независимы друг от друга. К счастью, nameнакладывает заказ на вторые два.

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

Карл Билефельдт
источник
2

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

Это не просто функциональное программирование; это обычно хорошая идея на любом языке. Если вы выполняете модульное тестирование, то, как вы разбиваете на части read_file(), convert()и write_file()получается совершенно естественно, потому что, несмотря на convert()то, что это самая сложная и большая часть кода, написание тестов для него относительно просто: все, что вам нужно настроить, это входной параметр , Написание тестов для read_file()и write_file()довольно сложно (даже если сами функции почти тривиальны), потому что вам нужно создавать и / или читать что-то в файловой системе до и после вызова функции. В идеале вы должны сделать такие функции настолько простыми, чтобы чувствовать себя комфортно, не проверяя их, и тем самым избавить себя от множества хлопот.

Разница между Python и Haskell заключается в том, что в Haskell есть средство проверки типов, которое может доказать, что функции не имеют побочных эффектов. В Python нужно надеяться, что никто не случайно вставил в функцию чтения или записи файлов convert()(скажем, read_config_file()). В Haskell, когда вы объявляете convert :: String -> Stringили подобное, без IOмонады, средство проверки типов гарантирует, что это чистая функция, которая полагается только на свой входной параметр и ничего больше. Если кто-то попытается изменить convertконфигурацию для чтения конфигурационного файла, он быстро увидит ошибки компилятора, показывающие, что он нарушает чистоту функции. (И, надеюсь, они будут достаточно разумны, чтобы read_config_fileвыйти convertи передать свой результат convert, поддерживая чистоту.)

Курт Дж. Сэмпсон
источник