Programmazione funzionale e algoritmi con stato


12

Sto imparando la programmazione funzionale con Haskell . Nel frattempo sto studiando la teoria degli automi e dato che i due sembrano stare bene insieme, sto scrivendo una piccola biblioteca per giocare con gli automi.

Ecco il problema che mi ha fatto porre la domanda. Mentre studiavo un modo per valutare la raggiungibilità di uno stato, ho avuto l'idea che un semplice algoritmo ricorsivo sarebbe abbastanza inefficiente, perché alcuni percorsi potrebbero condividere alcuni stati e potrei finire per valutarli più di una volta.

Ad esempio, qui, valutando raggiungibilità di g da una , avrei escludere f sia durante il controllo del percorso attraverso d e c :

digraph che rappresenta un automa

Quindi la mia idea è che un algoritmo che lavora in parallelo su molti percorsi e aggiorni un record condiviso di stati esclusi potrebbe essere eccezionale, ma questo è troppo per me.

Ho visto che in alcuni semplici casi di ricorsione si può passare lo stato come argomento, ed è quello che devo fare qui, perché passo avanti l'elenco degli stati che ho attraversato per evitare loop. Ma c'è un modo per passare indietro anche quella lista, come restituirla in una tupla insieme al risultato booleano della mia canReachfunzione? (anche se questo sembra un po 'forzato)

Oltre alla validità del mio caso di esempio , quali altre tecniche sono disponibili per risolvere questo tipo di problemi? Sento che questi devono essere abbastanza comuni da avere soluzioni come quello che succede con fold*o map.

Finora, leggendo learnyouahaskell.com non ne ho trovato nessuno, ma considera che non ho ancora toccato le monadi.

( se interessati, ho pubblicato il mio codice su codereview )


3
Ad esempio, mi piacerebbe vedere il codice con cui hai provato a lavorare. In mancanza di ciò, il mio miglior consiglio è che la pigrizia di Haskell può spesso essere sfruttata per non calcolare le cose più di una volta. Esamina il cosiddetto "legare il nodo" e la ricorsione del valore pigro, anche se il tuo problema è probabilmente abbastanza semplice che le tecniche più avanzate che sfruttano valori infiniti e cose simili sarebbero eccessive e probabilmente ti confonderebbero proprio ora.
Ptharien's Flame,

1
@ Ptharien'sFlame grazie per il tuo interesse! ecco il codice , c'è anche un link all'intero progetto. Sono già confuso con quello che ho fatto finora, quindi sì, meglio non esaminare le tecniche avanzate :)
bigstones

1
Gli automi di stato sono praticamente l'antitesi della programmazione funzionale. La programmazione funzionale riguarda la risoluzione di problemi senza stato interno, mentre gli automi di stato riguardano la gestione del proprio stato.
Philipp

@Philipp Non sono d'accordo. Un automa o una macchina a stati è talvolta il modo più naturale e preciso per rappresentare un problema e gli automi funzionali sono ben studiati.
Ptharien's Flame,

5
@Philipp: la programmazione funzionale consiste nel rendere esplicito lo stato, non nel proibirlo. In effetti, la ricorsione della coda è uno strumento davvero eccezionale per implementare quelle macchine statali piene di goto.
hugomg,

Risposte:


16

La programmazione funzionale non elimina lo stato. Lo rende solo esplicito! Mentre è vero che funzioni come map spesso "svelano" una struttura di dati "condivisa", se tutto ciò che vuoi fare è scrivere un algoritmo di raggiungibilità, allora è solo una questione di tenere traccia di quali nodi hai già visitato:

import qualified Data.Set as S
data Node = Node Int [Node] deriving (Show)

-- Receives a root node, returns a list of the node keyss visited in a depth-first search
dfs :: Node -> [Int]
dfs x = fst (dfs' (x, S.empty))

-- This worker function keeps track of a set of already-visited nodes to ignore.
dfs' :: (Node, S.Set Int) -> ([Int], S.Set Int)
dfs' (node@(Node k ns), s )
  | k  `S.member` s = ([], s)
  | otherwise =
    let (childtrees, s') = loopChildren ns (S.insert k s) in
    (k:(concat childtrees), s')

--This function could probably be implemented as just a fold but Im lazy today...
loopChildren :: [Node] -> S.Set Int -> ([[Int]], S.Set Int)
loopChildren []  s = ([], s)
loopChildren (n:ns) s =
  let (xs, s') = dfs' (n, s) in
  let (xss, s'') = loopChildren ns s' in
  (xs:xss, s'')

na = Node 1 [nb, nc, nd]
nb = Node 2 [ne]
nc = Node 3 [ne, nf]
nd = Node 4 [nf]
ne = Node 5 [ng]
nf = Node 6 []
ng = Node 7 []

main = print $ dfs na -- [1,2,5,7,3,6,4]

Ora, devo confessare che tenere traccia di tutto questo stato a mano è piuttosto fastidioso e soggetto a errori (è facile usare s 'invece di s' ', è facile passare la stessa s a più di un calcolo ...) . È qui che entrano in gioco le monadi: non aggiungono nulla che non si possa già fare prima, ma consentono di passare implicitamente la variabile di stato e l'interfaccia garantisce che ciò avvenga in un modo a thread singolo.


Modifica: cercherò di fornire un ragionamento più approfondito su ciò che ho fatto ora: prima di tutto, invece di testare la raggiungibilità, ho codificato una ricerca approfondita. L'implementazione sembrerà più o meno la stessa ma il debug sembra un po 'migliore.

In un linguaggio di stato, il DFS sarebbe simile a questo:

visited = set()  #mutable state
visitlist = []   #mutable state
def dfs(node):
   if isMember(node, visited):
       //do nothing
   else:
       visited[node.key] = true           
       visitlist.append(node.key)
       for child in node.children:
         dfs(child)

Ora dobbiamo trovare un modo per sbarazzarci dello stato mutevole. Prima di tutto ci liberiamo della variabile "visitlist" facendo restituire dfs che invece di void:

visited = set()  #mutable state
def dfs(node):
   if isMember(node, visited):
       return []
   else:
       visited[node.key] = true
       return [node.key] + concat(map(dfs, node.children))

E ora arriva la parte difficile: sbarazzarsi della variabile "visitata". Il trucco di base è usare una convenzione in cui passiamo lo stato come parametro aggiuntivo alle funzioni che ne hanno bisogno e fanno sì che tali funzioni restituiscano la nuova versione dello stato come valore di ritorno extra se vogliono modificarlo.

let increment_state s = s+1 in
let extract_state s = (s, 0) in

let s0 = 0 in
let s1 = increment_state s0 in
let s2 = increment_state s1 in
let (x, s3) = extract_state s2 in
-- and so on...

Per applicare questo modello al dfs, è necessario modificarlo per ricevere il set "visitato" come parametro aggiuntivo e restituire la versione aggiornata di "visitato" come valore di ritorno aggiuntivo. Inoltre, dobbiamo riscrivere il codice in modo da trasmettere sempre la versione "più recente" dell'array "visitato":

def dfs(node, visited1):
   if isMember(node, visited1):
       return ([], visited1) #return the old state because we dont want to  change it
   else:
       curr_visited = insert(node.key, visited1) #immutable update, with a new variable for the new value
       childtrees = []
       for child in node.children:
          (ct, curr_visited) = dfs(child, curr_visited)
          child_trees.append(ct)
       return ([node.key] + concat(childTrees), curr_visited)

La versione di Haskell fa praticamente quello che ho fatto qui, tranne che va fino in fondo e utilizza una funzione ricorsiva interna invece di variabili mutabili "curr_visited" e "childtrees".


Per quanto riguarda le monadi, ciò che sostanzialmente realizzano è implicitamente passare il "curr_visited" in giro, invece di costringerti a farlo a mano. Questo non solo rimuove il disordine dal codice, ma ti impedisce anche di fare errori, come lo stato di fork (passare lo stesso set "visitato" a due chiamate successive invece di concatenare lo stato).


Sapevo che doveva esserci un modo per renderlo meno doloroso e forse più leggibile, perché sto facendo fatica a capire il tuo esempio. Dovrei andare per le monadi o meglio esercitarmi di più per capire il codice come il tuo?
bigstones,

@bigstones: Penso che dovresti provare a capire come funziona il mio codice prima di affrontare le monadi: fondamentalmente faranno la stessa cosa che ho fatto ma con ulteriori livelli di astrazione per confonderti. Comunque, ho aggiunto qualche spiegazione in più per cercare di rendere le cose un po 'più chiare
hugomg,

1
"La programmazione funzionale non si libera dello stato. Lo rende solo esplicito!": Questo è davvero chiarente!
Giorgio,

"[Monads] ​​ti consente di passare implicitamente la variabile di stato in modo implicito e l'interfaccia garantisce che ciò avvenga in un modo a thread singolo" <- Questa è una descrizione illuminante delle monadi; al di fuori del contesto di questa domanda, posso sostituire "variabile di stato" con "chiusura"
androide antropico

2

Ecco una semplice risposta su cui fare affidamento mapConcat.

 mapConcat :: (a -> [b]) -> [a] -> [b]
 -- mapConcat is in the std libs, mapConcat = concat . map
 type Path = []

 isReachable :: a -> Auto a -> a -> [Path a]
 isReachable to auto from | to == from = [[]]
 isReachable to auto from | otherwise = 
    map (from:) . mapConcat (isReachable to auto) $ neighbors auto from

Dove neighborsrestituisce gli stati immediatamente connessi a uno stato. Ciò restituisce una serie di percorsi.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.