Сохранение состояния на чисто функциональном языке

Я пытаюсь понять, как это сделать, предположим, что вы работаете над контроллером для двигателя постоянного тока, который вы хотите, чтобы он вращался с определенной скоростью, установленной пользователем,


(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 по-прежнему чист и ведет себя надежно, и я ничего не могу с этим поделать, это изменит это.

Ответ 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.