Извините за еще один вопрос о побочных эффектах 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 должен использовать монады, если они уже не находятся в таком конвейере.
Есть ли какое-то дополнительное преимущество в борьбе с побочными эффектами, которые приносит паттерн ввода / вывода, чего мне не хватает?
main
в программе на HaskellIO ()
- действие IO. На самом деле это не функция; это ценность . Вся ваша программа представляет собой чистое значение, содержащее инструкции, которые сообщают языковой среде выполнения, что она должна делать. Все нечистое (фактически выполняющее действия ввода-вывода) выходит за рамки вашей программы.read_file
) и используете его в качестве аргумента для следующего (write_file
). Если бы у вас была только последовательность независимых действий, вам не понадобилась бы монада.Ответы:
Это та часть, в которой, я думаю, вы не видите этого с точки зрения Хаскеллера. Итак, у нас есть такая программа:
Я думаю, что типичный подход Хаскеллера к этому будет
convert
чистой частью:IO
части;IO
вообще.Таким образом, они не видят это как
convert
«заключенное в себе»IO
, а скорее как его изолированное отIO
. От его типа, что бы ниconvert
делало, никогда не может зависеть от того, что происходит вIO
действии.Я бы сказал, что это разделяется на две вещи:
convert
зависит от состояния файла.convert
функция делает , что не зависит от состояния файла.convert
всегда одна и та же функция , даже если она вызывается с разными аргументами в разных точках.Это несколько абстрактный вопрос, но это действительно ключ к пониманию того, что имеют в виду Хаскеллерс, когда говорят об этом. Вы хотите написать
convert
так, чтобы при любом действительном аргументе он приводил к правильному результату для этого аргумента. Когда вы смотрите на это так, то, что чтение файла является операцией с состоянием, не входит в уравнение; все, что имеет значение, - это то, что любой аргумент, который ему подают и откуда бы он ни исходил,convert
должен обрабатывать его правильно. И тот факт, что чистота ограничивает то, чтоconvert
можно сделать с его входом, упрощает эти рассуждения.Поэтому, если
convert
из некоторых аргументов получаются неверные результаты иreadFile
передаются им в качестве аргумента, мы не видим в этом ошибки, представленной состоянием . Это ошибка в чистой функции!источник
Трудно точно понять, что вы имеете в виду под «чисто академическим», но я думаю, что ответ в основном «нет».
Как объяснено в « Борьбе с неуклюжим отрядом » Саймона Пейтона Джонса ( сильно рекомендуется к прочтению!), Монадический ввод-вывод предназначался для решения реальных проблем способом, используемым Haskell для обработки ввода-вывода. Прочитайте пример сервера с запросами и ответами, который я не буду здесь копировать; это очень поучительно.
Haskell, в отличие от Python, поощряет стиль «чистых» вычислений, который может быть реализован системой типов. Конечно, вы можете использовать самодисциплину при программировании на Python, чтобы соответствовать этому стилю, но как насчет модулей, которые вы не написали? Без особой помощи со стороны системы типов (и общих библиотек) монадический ввод-вывод, вероятно, менее полезен в Python. Философия языка не предназначена для строгого разделения между чистым и нечистым.
Обратите внимание, что это говорит больше о различных философиях Haskell и Python, чем о том, как академический монадический ввод / вывод. Я бы не использовал его для Python.
Еще одна вещь. Ты говоришь:
Это правда, что
main
функция Haskell «живет»IO
, но реальным программам на Haskell рекомендуется не использовать ее,IO
когда она не нужна. Почти каждая функция, которую вы пишете, которая не нуждается в вводе / выводе, не должна иметь типIO
.Так что я бы сказал, что в последнем примере вы получили это задом наперед:
main
нечисто (потому что он читает и записывает файлы), но основные функции вроде быconvert
чисты.источник
Почему IO нечист? Потому что он может возвращать разные значения в разное время. Существует зависимость от времени, которую необходимо учитывать, так или иначе. Это еще более важно с ленивой оценкой. Рассмотрим следующую программу:
Без монады ввода-вывода, почему первая подсказка когда-либо получит вывод? От этого ничего не зависит, поэтому ленивая оценка означает, что она никогда не будет востребована. Также нет ничего, что заставляло бы выводиться до чтения ввода. Что касается компьютера, то без монады ввода-вывода эти первые два выражения полностью независимы друг от друга. К счастью,
name
накладывает заказ на вторые два.Существуют и другие способы решения проблемы зависимости порядка, но использование монады ввода-вывода, вероятно, является самым простым способом (по крайней мере с языковой точки зрения), позволяющим всему оставаться в ленивом функциональном царстве без небольших разделов императивного кода. Это также самый гибкий. Например, вы можете относительно легко построить динамический конвейер ввода-вывода во время выполнения на основе пользовательского ввода.
источник
Это не просто функциональное программирование; это обычно хорошая идея на любом языке. Если вы выполняете модульное тестирование, то, как вы разбиваете на части
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
, поддерживая чистоту.)источник