Для целей этого ответа я определяю «чисто функциональный язык» для обозначения функционального языка, в котором функции являются ссылочно-прозрачными, то есть многократный вызов одной и той же функции с одинаковыми аргументами всегда будет приводить к одним и тем же результатам. Это, я считаю, обычное определение чисто функционального языка.
Чисто функциональные языки программирования не допускают побочных эффектов (и поэтому практически бесполезны, поскольку любая полезная программа имеет побочные эффекты, например, когда она взаимодействует с внешним миром).
Самый простой способ достижения ссылочной прозрачности действительно состоит в том, чтобы запретить побочные эффекты, и действительно есть языки, на которых это имеет место (в основном доменные). Однако это, конечно, не единственный способ, и большинство чисто функциональных языков общего назначения (Haskell, Clean, ...) допускают побочные эффекты.
Кроме того, говорить, что язык программирования без побочных эффектов мало полезен на практике, на самом деле, на мой взгляд, не совсем справедливо - конечно, не для языков, специфичных для предметной области, но даже для языков общего назначения, я мог бы представить, что язык может быть весьма полезным без предоставления побочных эффектов , Возможно, не для консольных приложений, но я думаю, что приложения с графическим интерфейсом могут быть реализованы без побочных эффектов, скажем, в функционально-реактивной парадигме.
Что касается пункта 1, вы можете взаимодействовать со средой на чисто функциональных языках, но вы должны явно пометить код (функции), который их вводит (например, в Haskell с помощью монадических типов).
Это немного упрощает. Просто наличие системы, в которой функции с побочными эффектами должны быть помечены как таковые (аналогично const -корректности в C ++, но с общими побочными эффектами), недостаточно для обеспечения ссылочной прозрачности. Вы должны убедиться, что программа никогда не сможет вызывать функцию несколько раз с одинаковыми аргументами и получать разные результаты. Вы можете сделать это, сделав такие вещи, какreadLine
быть чем-то, что не является функцией (это то, что Haskell делает с монадой ввода-вывода), или вы можете лишить возможности вызывать побочные функции несколько раз с одним и тем же аргументом (это то, что делает Clean). В последнем случае компилятор будет гарантировать, что каждый раз, когда вы вызываете побочную функцию, вы делаете это со свежим аргументом, и он отклоняет любую программу, в которой вы передаете один и тот же аргумент побочной функции дважды.
Чистые функциональные языки программирования не позволяют написать программу, которая поддерживает состояние (что делает программирование очень неудобным, потому что во многих приложениях вам нужно состояние).
Опять же, чисто функциональный язык вполне может запретить изменяемое состояние, но, безусловно, возможно быть чистым и при этом иметь изменяемое состояние, если вы реализуете его так же, как я описал с побочными эффектами выше. Действительно изменяемое состояние - это просто еще одна форма побочных эффектов.
Тем не менее, функциональные языки программирования определенно препятствуют изменчивому состоянию - особенно чистому. И я не думаю, что это делает программирование неловким - совсем наоборот. Иногда (но не все так часто) невозможно избежать изменяемого состояния без потери производительности или ясности (именно поэтому языки, подобные Haskell, имеют возможности для изменяемого состояния), но чаще всего это возможно.
Если это неправильные представления, как они возникли?
Я думаю, что многие люди просто читают «функция должна вызывать одинаковый результат при вызове с одинаковыми аргументами» и делают из этого вывод, что невозможно реализовать что-то подобное readLine
или код, который поддерживает изменяемое состояние. Таким образом, они просто не знают о «читах», которые могут использовать чисто функциональные языки, чтобы представить эти вещи без нарушения ссылочной прозрачности.
Кроме того, изменяемое состояние сильно не поощряется в функциональных языках, поэтому не так уж и сложно сделать вывод, что его вообще нельзя использовать в чисто функциональных.
Не могли бы вы написать (возможно небольшой) фрагмент кода, иллюстрирующий идиоматический способ Haskell для (1) реализации побочных эффектов и (2) реализации вычислений с состоянием?
Вот приложение в Pseudo-Haskell, которое запрашивает у пользователя имя и приветствует его. Pseudo-Haskell - это язык, который я только что изобрел, который имеет систему ввода-вывода Haskell, но использует более обычный синтаксис, более описательные имена функций и не имеет do
-notation (поскольку это просто отвлекает от того, как именно работает монада IO):
greet(name) = print("Hello, " ++ name ++ "!")
main = composeMonad(readLine, greet)
Подсказка в том, что 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
.
Изменяемое состояние будет выглядеть так:
counter = mutableVariable(0)
increaseCounter(cnt) =
setIncreasedValue(oldValue) = setValue(cnt, oldValue + 1)
composeMonad(getValue(cnt), setIncreasedValue)
printCounter(cnt) = composeMonad( getValue(cnt), print )
main = composeVoidMonad( increaseCounter(counter), printCounter(counter) )
Вот mutableVariable
функция, которая принимает значение любого типа T
и создает MutableVariable<T>
. Функция getValue
принимает MutableVariable
и возвращает значение, IO<T>
которое выдает текущее значение. setValue
принимает a MutableVariable<T>
и a T
и возвращает значение, IO<void>
которое устанавливает значение. composeVoidMonad
то же самое, composeMonad
за исключением того, что первый аргумент является аргументом IO
, который не дает разумного значения, а второй аргумент является другой монадой, а не функцией, которая возвращает монаду.
В Haskell есть некоторый синтаксический сахар, который делает все это испытание менее болезненным, но все же очевидно, что изменчивое состояние - это то, что язык на самом деле не хочет, чтобы вы делали.