Для этого ответа я собираюсь интерпретировать «чисто функциональный язык» как означающий «язык в стиле ML, исключающий побочные эффекты», который я, в свою очередь, буду интерпретировать как означающий «Haskell», который я буду интерпретировать как значение"GHC".Ничто из этого не является строго правдивым, но, учитывая, что вы сравниваете это с производным от Лиспа и что GHC довольно заметен, я предполагаю, что это все равно станет основой вашего вопроса.
Как всегда,Ответ на Haskell - это ловкость рук, когда доступ к изменяемым данным (или к чему-либо с побочными эффектами) структурирован таким образом, что система типов гарантирует, что она будет «выглядеть» чисто изнутри, производяокончательная программа, которая имеет побочные эффекты, где ожидается.Обычный бизнес с монадами - большая часть этого, но детали не имеют большого значения и в основном отвлекают от проблемы.На практике это просто означает, что вы должны четко указать, где могут возникнуть побочные эффекты и в каком порядке, и вам не разрешено «обманывать».
Примитивы изменчивости обычно предоставляются языковой средой выполнения, идоступ через функции, которые производят значения в некоторой монаде, также предоставляемой средой выполнения (часто IO
, иногда более специализированными).Во-первых, давайте посмотрим на предоставленный вами пример Clojure: он использует ref
, который описан в документации здесь :
В то время как Vars обеспечивает безопасное использование изменяемого хранилищаместоположения посредством изоляции потоков, ссылки на транзакции (Refs) обеспечивают безопасное совместное использование изменяемых хранилищ через систему программной транзакционной памяти (STM).Ссылки привязаны к одному хранилищу в течение всего срока их службы и допускают только мутацию этого местоположения в транзакции.
Забавно, что весь этот абзац довольно прямо переводится в GHC Haskell.Я предполагаю, что «Vars» эквивалентны Haskell MVar
, в то время как «Refs» почти наверняка эквивалентны TVar
, как указано в пакете stm
.
Итак, чтобы перевести пример на Haskell, нам понадобится функция, которая создает TVar
:
setPoint :: STM (TVar Int)
setPoint = newTVar 90
... и мы можем использовать ее в коде, подобном этому:
updateLoop :: IO ()
updateLoop = do tvSetPoint <- atomically setPoint
sequence_ . repeat $ update tvSetPoint
where update tv = do curSpeed <- readSpeed
curSet <- atomically $ readTVar tv
controller curSet curSpeed
При реальном использовании мой код был бы гораздо более кратким, но я оставил здесь более подробные данные в надежде быть менее загадочным.
Полагаю, можно возразить, что этот кодне чистый и использует изменяемое состояние, но ... ну и что?В какой-то момент программа будет запущена, и мы хотели бы, чтобы она выполняла ввод и вывод.Важно то, что мы сохраняем все преимущества кода в чистоте даже при использовании его для написания кода с изменяемым состоянием.Например, я реализовал бесконечный цикл побочных эффектов, используя функцию repeat
;но repeat
все еще чист и ведет себя надежно, и ничего, что я могу с этим поделать, не изменит этого.