Domanda Mantenere lo stato in un linguaggio puramente funzionale


Sto cercando di capire come fare quanto segue, supponiamo che tu stia lavorando su un controller per un motore DC che vuoi continuare a ruotare a una certa velocità impostata dall'utente,


(def set-point (ref {:sp 90}))

(while true
  (let [curr (read-speed)]
    (controller @set-point curr)))


Ora che il set-point può cambiare in qualsiasi momento tramite un'applicazione web, non riesco a pensare a un modo per farlo senza usare ref, quindi la mia domanda è come i linguaggi funzionali si occupano di questo genere di cose? (anche se l'esempio è in clojure mi interessa l'idea generale).


10
2018-05-20 11:17


origine


risposte:


Questo non risponderà alla tua domanda, ma voglio mostrare come sono fatte queste cose in Clojure. Potrebbe aiutare qualcuno a leggerlo più tardi, in modo che non pensi di dover leggere su monadi, programmazione reattiva o altri soggetti "complicati" per usare Clojure.

Clojure non è un puramente linguaggio funzionale e in questo caso potrebbe essere una buona idea lasciare da parte le funzioni pure per un momento e un modello lo stato intrinseco del sistema con le identità.

In Clojure, probabilmente useresti uno dei tipi di riferimento. Ci sono molti tra cui scegliere e sapere quale usare potrebbe essere difficile. La buona notizia è che tutti supportano il modello di aggiornamento unificato quindi cambiare il tipo di riferimento in un secondo momento dovrebbe essere abbastanza semplice.

Ho scelto un atom ma a seconda delle esigenze potrebbe essere più appropriato usare a ref o un agent.

Il motore è un'identità nel tuo programma. È un "marchio" per alcuni cosa che ha valori diversi in tempi diversi e questi valori sono correlati tra loro (cioè la velocità del motore). Ho messo un :validator sull'atomo per garantire che la velocità non scenda mai sotto lo zero.

(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

Se vuoi cambiare la semantica dell'identità motoria hai almeno altri due tipi di riferimento tra cui scegliere.

  • Se si desidera modificare la velocità del motore in modo asincrono si utilizzerà un agente. Quindi devi cambiare swap! con send. Ciò sarebbe utile se, ad esempio, i client che regolano la velocità del motore sono diversi dai client che utilizzano la velocità del motore, quindi è bene che la velocità venga modificata "alla fine".

  • Un'altra opzione è usare a ref che sarebbe appropriato se il motore ha bisogno di coordinarsi con altre identità nel tuo sistema. Se scegli questo tipo di riferimento che cambi swap! con alter. Inoltre, tutte le modifiche di stato vengono eseguite in una transazione con dosync per garantire che tutte le identità nella transazione siano aggiornate atomicamente.

Le Monade non sono necessarie per modellare identità e stato in Clojure!


15
2018-05-20 14:00



Per questa risposta, interpreterò "un linguaggio puramente funzionale" come "un linguaggio in stile ML che esclude effetti collaterali" che interpreterò a sua volta nel significato di "Haskell" che interpreterò come "GHC" . Nessuno di questi è assolutamente vero, ma dato che stai contrastando questo con un derivato Lisp e che GHC è piuttosto prominente, immagino che questo sarà comunque al centro della tua domanda.

Come sempre, la risposta in Haskell è un po 'di gioco di prestigio in cui l'accesso a dati mutabili (o qualsiasi cosa con effetti collaterali) è strutturato in modo tale che il sistema di tipo garantisce che "sembrerà" puro dall'interno, mentre produce un programma finale che ha effetti collaterali dove previsto. La solita attività commerciale con le monadi è una grande parte di questo, ma i dettagli non contano davvero e per lo più distolgono il problema. In pratica, significa solo che devi essere esplicito su dove possono verificarsi gli effetti collaterali e in quale ordine, e non ti è permesso "imbrogliare".

Le primitive di mutabilità sono generalmente fornite dal runtime del linguaggio e sono accessibili tramite funzioni che producono valori in alcuni monad forniti anche dal runtime (spesso IO, a volte più specializzati). Per prima cosa, diamo un'occhiata all'esempio Clojure che hai fornito: utilizza ref, che è descritto in la documentazione qui:

Mentre Vars garantisce l'uso sicuro delle posizioni di memoria mutabili tramite isolamento del filo, i riferimenti transazionali (Refs) garantiscono un utilizzo condiviso e sicuro delle posizioni di memoria mutabili tramite un sistema di memoria transazionale software (STM). I Refs sono legati a una singola posizione di archiviazione per tutta la loro durata e consentono solo la mutazione di tale posizione di verificarsi all'interno di una transazione.

Divertente, l'intero paragrafo si traduce piuttosto direttamente in GHC Haskell. Immagino che "Vars" sia equivalente a Haskell di MVar, mentre "Refs" sono quasi certamente equivalenti a TVar come trovato nel stm pacchetto.

Quindi per tradurre l'esempio in Haskell, avremo bisogno di una funzione che crei il TVar:

setPoint :: STM (TVar Int)
setPoint = newTVar 90

... e possiamo usarlo in codice come questo:

updateLoop :: IO ()
updateLoop = do tvSetPoint <- atomically setPoint
                sequence_ . repeat $ update tvSetPoint
  where update tv = do curSpeed <- readSpeed
                       curSet   <- atomically $ readTVar tv
                       controller curSet curSpeed

In realtà il mio codice sarebbe molto più tetro di quello, ma ho lasciato le cose più prolisse qui nella speranza di essere meno criptici.

Suppongo che si possa obiettare che questo codice non è puro e utilizza uno stato mutevole, ma ... e allora? A un certo punto un programma sta per essere eseguito e vorremmo che facesse input e output. L'importante è che conserviamo tutto il benefici di codice puro, anche quando lo si utilizza per scrivere codice con stato mutabile. Ad esempio, ho implementato un ciclo infinito di effetti collaterali usando il repeat funzione; ma repeat è ancora puro e si comporta in modo affidabile e nulla di ciò che posso fare cambierà tutto ciò.


8
2018-05-20 14:20



Una tecnica per affrontare problemi apparentemente urlare per la mutabilità (come la GUI o le applicazioni web) in modo funzionale Programmazione reattiva funzionale.


3
2018-05-20 11:46



Il modello di cui hai bisogno per questo è chiamato Monade. Se vuoi davvero entrare nella programmazione funzionale dovresti cercare di capire per cosa sono usate le monadi e cosa possono fare. Come punto di partenza vorrei suggerire questo link.

Come una breve spiegazione informale per le monadi:

Le Monade possono essere viste come dati + contesto che vengono passati nel tuo programma. Questa è la "tuta spaziale" usata spesso nelle spiegazioni. Passi i dati e il contesto intorno e inserisci qualsiasi operazione in questa Monade. Di solito non c'è modo di recuperare i dati una volta che sono stati inseriti nel contesto, puoi semplicemente fare il contrario inserendo le operazioni, in modo che gestiscano i dati combinati con il contesto. In questo modo sembra quasi che i dati vengano estratti, ma se guardi da vicino non lo fai mai.

A seconda dell'applicazione, il contesto può essere praticamente qualsiasi cosa. Una struttura dati che combina più entità, eccezioni, opzioni o il mondo reale (i / o-monadi). Nella carta collegata sopra il contesto saranno gli stati di esecuzione di un algoritmo, quindi questo è abbastanza simile alle cose che hai in mente.


2
2018-05-20 11:37



In Erlang potresti usare un processo per mantenere il valore. Qualcosa come questo:

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.

2
2018-05-20 18:44