Сохранение состояния на чисто функциональном языке
Я пытаюсь понять, как это сделать, предположим, что вы работаете над контроллером для двигателя постоянного тока, который вы хотите, чтобы он вращался с определенной скоростью, установленной пользователем,
(def set-point (ref {:sp 90}))
(while true
(let [curr (read-speed)]
(controller @set-point curr)))
Теперь, когда заданная точка может меняться в любое время через веб-приложение, я не могу придумать способ сделать это, не используя ref, поэтому мой вопрос заключается в том, как функциональные языки имеют дело с такими вещами? (хотя пример находится в clojure, меня интересует общая идея.)
Ответы
Ответ 1
Это не ответит на ваш вопрос, но я хочу показать, как это делается в Clojure. Это может помочь кому-то прочитать это позже, чтобы они не думали, что им нужно читать монад, реактивное программирование или другие "сложные" темы для использования Clojure.
Clojure не является функциональным языком чисто, и в этом случае может быть хорошей идеей оставить на некоторое время чистые функции и модель неотъемлемое состояние системы с идентификаторами.
В Clojure вы, вероятно, используете один из ссылочных типов. Есть несколько вариантов выбора и знать, какой из них использовать может быть сложно. Хорошей новостью является то, что все они поддерживают единую модель обновления, поэтому изменение ссылочного типа позже должно быть довольно простым.
Я выбрал atom
, но в зависимости от ваших требований было бы более целесообразным использовать ref
или agent
.
Двигатель - это идентификатор вашей программы. Это "метка" для какой-то вещи, которая имеет разные значения в разное время, и эти значения связаны друг с другом (т.е. Скорость двигателя). Я положил a :validator
на атом, чтобы гарантировать, что скорость никогда не опускается ниже нуля.
(def motor (atom {:speed 0} :validator (comp not neg? :speed)))
(defn add-speed [n]
(swap! motor update-in [:speed] + n))
(defn set-speed [n]
(swap! motor update-in [:speed] (constantly n)))
> (add-speed 10)
> (add-speed -8)
> (add-speed -4) ;; This will not change the state of motor
;; since the speed would drop below zero and
;; the validator does not allow that!
> (:speed @motor)
2
> (set-speed 12)
> (:speed @motor)
12
Если вы хотите изменить семантику идентификатора мотора, вы можете выбрать по крайней мере два других ссылочных типа.
-
Если вы хотите изменить скорость асинхронного двигателя, вы должны использовать агент. Затем вам нужно изменить swap!
на send
. Это было бы полезно, если, например, клиенты, настраивающие скорость двигателя, отличаются от клиентов, использующих скорость двигателя, так что это нормально для скорости, которую нужно изменить "в конце концов".
-
Другим вариантом является использование ref
, которое было бы подходящим, если бы двигатель нуждался в координации с другими идентификаторами в вашей системе. Если вы выберете этот тип ссылки, вы измените swap!
на alter
. Кроме того, все изменения состояния выполняются в транзакции с dosync
, чтобы гарантировать, что все тождества в транзакции обновляются атомарно.
Монады не нужны для моделирования идентичности и состояния в Clojure!
Ответ 2
Для этого ответа я собираюсь интерпретировать "чисто функциональный язык" как "язык ML-стиля, который исключает побочные эффекты", который я буду интерпретировать в свою очередь как значение "Haskell", которое я буду интерпретировать как значение "GHC". Ни одно из них не является строго истинным, но учитывая, что вы сравниваете это с производным Lisp и что GHC довольно заметен, я предполагаю, что это все равно будет в центре вашего вопроса.
Как всегда, ответ в Haskell - это немного сообразительности, когда доступ к изменяемым данным (или что-либо с побочными эффектами) структурирован таким образом, что система типов гарантирует, что она будет "выглядеть" чистой от внутри, производя окончательную программу, которая имеет побочные эффекты там, где это ожидалось. Обычное дело с монадами - большая часть этого, но детали не имеют большого значения и в основном отвлекают от проблемы. На практике это просто означает, что вы должны быть четко о том, где могут возникать побочные эффекты и в каком порядке, и вам не разрешается "обманывать".
Примитивы взаимозаменяемости обычно предоставляются языковой средой выполнения и доступны через функции, которые производят значения в некоторой монаде, также предоставляемые средой выполнения (часто IO
, а иногда и более специализированные). Во-первых, давайте рассмотрим пример Clojure, который вы указали: он использует ref
, который описан в документации здесь:
В то время как Vars обеспечивает безопасное использование изменяемых хранилищ посредством изоляции потоков, транзакционные ссылки (Refs) обеспечивают безопасное совместное использование измененных мест хранения через систему транзакционной памяти программного обеспечения (STM). Refs привязаны к одному месту хранения в течение их жизненного цикла и позволяют разрешить мутацию этого местоположения в транзакции.
Интересно, что весь абзац переводится довольно прямо в 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
по-прежнему чист и ведет себя надежно, и я ничего не могу с этим поделать, это изменит это.
Ответ 3
Метод решения проблем, которые, по-видимому, кричат о мутируемости (например, графическом интерфейсе или веб-приложениях) функциональным способом, Функциональное реактивное программирование.
Ответ 4
Образец, который вам нужен для этого, называется Monads. Если вы действительно хотите попасть в функциональное программирование, вы должны попытаться понять, для чего используются монады и что они могут делать. В качестве отправной точки я бы предложил эту ссылку.
В качестве короткого неофициального объяснения для монад:
Монады можно рассматривать как данные + контекст, который передается в вашей программе. Это "космический костюм", который часто используется в объяснениях. Вы передаете данные и контекст вместе и вставляете любую операцию в эту Monad. Как правило, нет возможности вернуть данные после его вставки в контекст, вы можете просто выполнить операции вставки в обратном порядке, чтобы они обрабатывали данные в сочетании с контекстом. Таким образом, кажется, что вы получаете данные, но если внимательно присмотреться, вы никогда этого не делаете.
В зависимости от вашего приложения контекст может быть почти любым. Датструктура, объединяющая несколько объектов, исключений, опций или реальный мир (i/o-monads). В документе, связанном выше, контекст будет состоянием выполнения алгоритма, так что это очень похоже на то, что вы имеете в виду.
Ответ 5
В Erlang вы можете использовать процесс для хранения значения. Что-то вроде этого:
holdVar(SomeVar) ->
receive %% wait for message
{From, get} -> %% if you receive a get
From ! {value, SomeVar}, %% respond with SomeVar
holdVar(SomeVar); %% recursively call holdVar
%% to start listening again
{From, {set, SomeNewVar}} -> %% if you receive a set
From ! {ok}, %% respond with ok
holdVar(SomeNewVar); %% recursively call holdVar with
%% the SomeNewVar that you received
%% in the message
end.