Я часто сталкиваюсь со следующими утверждениями / аргументами:
- Чисто функциональные языки программирования не допускают побочных эффектов (и поэтому практически бесполезны, поскольку любая полезная программа имеет побочные эффекты, например, когда она взаимодействует с внешним миром).
- Чистые функциональные языки программирования не позволяют написать программу, которая поддерживает состояние (что делает программирование очень неудобным, потому что во многих приложениях вам нужно состояние).
Я не специалист по функциональным языкам, но вот что я понял по этим темам до сих пор.
Что касается пункта 1, вы можете взаимодействовать со средой на чисто функциональных языках, но вы должны явно пометить код (функции), который вводит побочные эффекты (например, в Haskell с помощью монадических типов). Кроме того, насколько я знаю, вычисление с помощью побочных эффектов (деструктивное обновление данных) также должно быть возможным (с использованием монадических типов?), Даже если это не предпочтительный способ работы.
Что касается пункта 2, насколько я знаю, вы можете представлять состояние, пропуская значения через несколько этапов вычисления (опять же, в Haskell, используя монадические типы), но у меня нет практического опыта в этом, и мое понимание довольно расплывчато.
Итак, верны ли два приведенных выше утверждения в каком-то смысле или это просто неправильные представления о чисто функциональных языках? Если это неправильные представления, как они возникли? Не могли бы вы написать (возможно небольшой) фрагмент кода, иллюстрирующий идиоматический способ Haskell для (1) реализации побочных эффектов и (2) реализации вычислений с состоянием?
источник
Ответы:
Для целей этого ответа я определяю «чисто функциональный язык» для обозначения функционального языка, в котором функции являются ссылочно-прозрачными, то есть многократный вызов одной и той же функции с одинаковыми аргументами всегда будет приводить к одним и тем же результатам. Это, я считаю, обычное определение чисто функционального языка.
Самый простой способ достижения ссылочной прозрачности действительно состоит в том, чтобы запретить побочные эффекты, и действительно есть языки, на которых это имеет место (в основном доменные). Однако это, конечно, не единственный способ, и большинство чисто функциональных языков общего назначения (Haskell, Clean, ...) допускают побочные эффекты.
Кроме того, говорить, что язык программирования без побочных эффектов мало полезен на практике, на самом деле, на мой взгляд, не совсем справедливо - конечно, не для языков, специфичных для предметной области, но даже для языков общего назначения, я мог бы представить, что язык может быть весьма полезным без предоставления побочных эффектов , Возможно, не для консольных приложений, но я думаю, что приложения с графическим интерфейсом могут быть реализованы без побочных эффектов, скажем, в функционально-реактивной парадигме.
Это немного упрощает. Просто наличие системы, в которой функции с побочными эффектами должны быть помечены как таковые (аналогично const -корректности в C ++, но с общими побочными эффектами), недостаточно для обеспечения ссылочной прозрачности. Вы должны убедиться, что программа никогда не сможет вызывать функцию несколько раз с одинаковыми аргументами и получать разные результаты. Вы можете сделать это, сделав такие вещи, как
readLine
быть чем-то, что не является функцией (это то, что Haskell делает с монадой ввода-вывода), или вы можете лишить возможности вызывать побочные функции несколько раз с одним и тем же аргументом (это то, что делает Clean). В последнем случае компилятор будет гарантировать, что каждый раз, когда вы вызываете побочную функцию, вы делаете это со свежим аргументом, и он отклоняет любую программу, в которой вы передаете один и тот же аргумент побочной функции дважды.Опять же, чисто функциональный язык вполне может запретить изменяемое состояние, но, безусловно, возможно быть чистым и при этом иметь изменяемое состояние, если вы реализуете его так же, как я описал с побочными эффектами выше. Действительно изменяемое состояние - это просто еще одна форма побочных эффектов.
Тем не менее, функциональные языки программирования определенно препятствуют изменчивому состоянию - особенно чистому. И я не думаю, что это делает программирование неловким - совсем наоборот. Иногда (но не все так часто) невозможно избежать изменяемого состояния без потери производительности или ясности (именно поэтому языки, подобные Haskell, имеют возможности для изменяемого состояния), но чаще всего это возможно.
Я думаю, что многие люди просто читают «функция должна вызывать одинаковый результат при вызове с одинаковыми аргументами» и делают из этого вывод, что невозможно реализовать что-то подобное
readLine
или код, который поддерживает изменяемое состояние. Таким образом, они просто не знают о «читах», которые могут использовать чисто функциональные языки, чтобы представить эти вещи без нарушения ссылочной прозрачности.Кроме того, изменяемое состояние сильно не поощряется в функциональных языках, поэтому не так уж и сложно сделать вывод, что его вообще нельзя использовать в чисто функциональных.
Вот приложение в Pseudo-Haskell, которое запрашивает у пользователя имя и приветствует его. Pseudo-Haskell - это язык, который я только что изобрел, который имеет систему ввода-вывода Haskell, но использует более обычный синтаксис, более описательные имена функций и не имеет
do
-notation (поскольку это просто отвлекает от того, как именно работает монада IO):Подсказка в том, что
readLine
это значение типаIO<String>
иcomposeMonad
функция, которая принимает аргумент типаIO<T>
(для некоторого типаT
) и другой аргумент, который является функцией, которая принимает аргумент типаT
и возвращает значение типаIO<U>
(для некоторого типаU
).print
это функция, которая принимает строку и возвращает значение типаIO<void>
.Значение типа
IO<A>
- это значение, которое «кодирует» данное действие, которое создает значение типаA
.composeMonad(m, f)
создает новоеIO
значение, которое кодирует действие, заm
которым следует действиеf(x)
, гдеx
- значение, полученное при выполнении действияm
.Изменяемое состояние будет выглядеть так:
Вот
mutableVariable
функция, которая принимает значение любого типаT
и создаетMutableVariable<T>
. ФункцияgetValue
принимаетMutableVariable
и возвращает значение,IO<T>
которое выдает текущее значение.setValue
принимает aMutableVariable<T>
и aT
и возвращает значение,IO<void>
которое устанавливает значение.composeVoidMonad
то же самое,composeMonad
за исключением того, что первый аргумент является аргументомIO
, который не дает разумного значения, а второй аргумент является другой монадой, а не функцией, которая возвращает монаду.В Haskell есть некоторый синтаксический сахар, который делает все это испытание менее болезненным, но все же очевидно, что изменчивое состояние - это то, что язык на самом деле не хочет, чтобы вы делали.
источник
counter
, то естьincreaseCounter(counter)
?main
будет тем, которое фактически будет выполнено. За исключением возврата ввода-выводаmain
, невозможно выполнить какие-либоIO
действия (без использования ужасно злых функций, имеющихunsafe
свое имя).IO
ценности. Я не понял, ссылается ли он на сопоставление с образцом, то есть на тот факт, что вы можете деконструировать значения алгебраического типа данных, но нельзя использовать сопоставление с образцом, чтобы сделать это соIO
значениями.ИМХО, вы сбиты с толку, потому что есть разница между чистым языком и чистой функцией . Давайте начнем с функции. Функция является чистой, если она (при одинаковых входных данных) всегда возвращает одно и то же значение и не вызывает никаких наблюдаемых побочных эффектов. Типичными примерами являются математические функции, такие как f (x) = x * x. Теперь рассмотрим реализацию этой функции. Это было бы чисто в большинстве языков, даже тех, которые обычно не считаются чисто функциональными языками, например, ML. Даже метод Java или C ++ с таким поведением можно считать чистым.
Так что же такое чистый язык? Строго говоря, можно ожидать, что чистый язык не позволяет выражать функции, которые не являются чистыми. Давайте назовем это идеалистическим определением чистого языка. Такое поведение очень желательно. Зачем? Хорошо, что в программе, состоящей только из чистых функций, вы можете заменить приложение функции его значением, не меняя смысла программы. Это позволяет очень легко рассуждать о программах, потому что когда вы знаете результат, вы можете забыть, как он был вычислен. Чистота может также позволить компилятору выполнять определенные агрессивные оптимизации.
Так что, если вам нужно внутреннее состояние? Вы можете имитировать состояние на чистом языке, просто добавляя состояние перед вычислением в качестве входного параметра и состояние после вычисления как часть результата. Вместо
Int -> Bool
тебя получается что-то вродеInt -> State -> (Bool, State)
. Вы просто делаете зависимость явной (что считается хорошей практикой в любой парадигме программирования). Кстати, есть монада, которая является особенно элегантным способом объединить такие функции имитации состояния в большие функции имитации состояния. Таким образом, вы определенно можете «поддерживать состояние» на чистом языке. Но вы должны сделать это явно.Значит ли это, что я могу взаимодействовать с внешним миром? Ведь полезная программа должна взаимодействовать с реальным миром, чтобы быть полезной. Но вход и выход явно не чистые. Запись конкретного байта в конкретный файл может быть хорошей в первый раз. Но выполнение точно такой же операции во второй раз может привести к ошибке, поскольку диск заполнен. Очевидно, что не существует чистого языка (в идеалистическом смысле), который мог бы записывать в файл.
Поэтому мы столкнулись с дилеммой. Мы хотим в основном чистые функции, но некоторые побочные эффекты абсолютно необходимы, и они не являются чистыми. Теперь реалистичным определением чистого языка будет то, что должны быть какие-то средства для отделения чистых частей от других частей. Механизм должен гарантировать, что никакая нечистая операция не проникнет в чистые части.
В Haskell это делается с помощью типа IO. Вы не можете уничтожить результат ввода-вывода (без небезопасных механизмов). Таким образом, вы можете обрабатывать результаты ввода-вывода только с помощью функций, определенных в самом модуле ввода-вывода. К счастью, есть очень гибкие комбинаторы, которые позволяют вам брать результат ввода-вывода и обрабатывать его в функции, пока эта функция возвращает другой результат ввода-вывода. Этот комбинатор называется bind (или
>>=
) и имеет типIO a -> (a -> IO b) -> IO b
. Если вы обобщите эту концепцию, вы придете к классу монад, и IO окажется его примером.источник
unsafe
ее именем) не соответствует вашему идеалистическому определению. В Haskell нет нечистых функций (опять игнорируемunsafePerformIO
и ко.).readFile
иwriteFile
всегда будет возвращать одно и то жеIO
значение, учитывая одинаковые аргументы. Так, например, два фрагмента кодаlet x = writeFile "foo.txt" "bar" in x >> x
иwriteFile "foo.txt" "bar" >> writeFile "foo.txt" "bar"
будут делать то же самое.IO Something
? Если это так, то вполне возможно дважды вызвать функцию ввода-вывода с одним и тем же аргументом:putStrLn "hello" >> putStrLn "hello"
- здесь оба вызоваputStrLn
имеют один и тот же аргумент. Конечно, это не проблема, потому что, как я сказал ранее, оба вызова приведут к одному и тому же значению ввода-вывода.writeFile "foo.txt" "bar"
не может вызвать ошибку, потому что оценка вызова функции не выполняет действие. Если вы говорите, что в моем предыдущем примере у версии сlet
есть только одна возможность вызвать сбой ввода-вывода, а у версии безlet
двух - вы ошибаетесь. Обе версии имеют две возможности для сбоя ввода-вывода. Посколькуlet
версия оценивает вызовwriteFile
только один раз, а версия безlet
его оценки дважды, вы можете видеть, что не имеет значения, как часто вызывается функция.putStrLn
Функция принимает только один аргумент, который имеет типString
. Если вы не верите мне, посмотрите на его тип:String -> IO ()
. Он, конечно, не принимает аргументов типаIO
- он генерирует значение этого типа.