Una delle idee fondamentali di programmazione funzionale è esprimere algoritmi come dati trasformazioni. In un linguaggio pigro come Haskell, possiamo anche fare un passo avanti e pensare a strutture dati pigre come calcoli reificati. In un senso molto reale, gli elenchi di Haskell sono più simili agli anelli delle normali liste concatenate: possono essere calcolati in modo incrementale e non devono necessariamente esistere in memoria tutti in una volta. Allo stesso tempo, otteniamo ancora molti dei vantaggi di avere un tipo di dati come tale capacità di passarlo e controllarlo con la corrispondenza dei modelli.
Con questo in mente, il "trucco" per esprimere un ciclo for con un indice è creare un elenco di tutti i valori che può assumere. Il vostro esempio è probabilmente il caso più semplice: i
prende tutti i valori da 0
a 255
, in modo che possiamo usare la notazione built-in di Haskell per campi:
[0..255]
Ad alto livello, questo è equivalente di Haskell di for (i = 0 to 255)
; possiamo quindi eseguire la logica effettiva nel ciclo attraversando questa lista tramite una funzione ricorsiva o una funzione di ordine superiore dalla libreria standard. (La seconda opzione è altamente preferita.)
Questa particolare logica è adatta per uno fold
. Una piega ci consente di prendere in esame una lista per elemento e creare un risultato di qualche tipo. Ad ogni passo, otteniamo una voce di elenco e il valore del nostro risultato accumulato finora. In questo caso particolare, vogliamo elaborare l'elenco da sinistra a destra mentre incrementiamo un indice, quindi possiamo usare foldl
; l'unica parte difficile è che produrrà la lista all'indietro.
Ecco il tipo di foldl
:
foldl :: (b -> a -> b) -> b -> [a] -> b
Quindi la nostra funzione assume nel nostro valore intermedio ed un elemento di lista e produce un valore intermedio aggiornato. Dato che stiamo costruendo un elenco e teniamo traccia di un indice, il nostro valore intermedio sarà una coppia che contiene entrambi. Poi, una volta che abbiamo il risultato finale, si può ignorare il valore idx
e invertire la lista finale otteniamo:
a = let (result, _) = foldl step ([], 1) [0..255] in reverse result
where step (a, idx) i
| someCondition i = (idx:a, idx + 1)
| otherwise = (0:a, idx)
Infatti, lo schema di trasformare un elenco tenendo traccia di uno stato intermedio (idx
in questo caso) è abbastanza comune in modo che abbia una sua funzione in termini di tipo State
. L'astrazione principale è un po 'più coinvolta (leggi attraverso ["Potresti aver inventato le Monade"] [tu] per un'ottima introduzione), ma il codice risultante è in realtà piuttosto piacevole da leggere (ad eccezione delle importazioni, immagino: P) :
import Control.Applicative
import Control.Monad
import Control.Monad.State
a = evalState (mapM step [0..255]) 1
where step i
| someCondition i = get <* modify (+ 1)
| otherwise = return 0
l'idea è che si mappa su [0..255]
tenendo traccia di qualche stato (il valore di idx
) nei precedenti. evalState
è come mettiamo insieme tutti gli impianti idraulici e otteniamo il nostro risultato finale. La funzione step
viene applicata a ciascun elemento dell'elenco di input e può anche accedere o modificare lo stato.
Il primo caso della funzione step
è interessante. L'operatore <*
gli dice di fare prima la cosa a sinistra, la cosa sulla seconda destra ma restituisce il valore a sinistra. Ciò ci consente di ottenere lo stato corrente, di incrementarlo ma di restituire il valore che abbiamo prima dello che è stato incrementato. Il fatto che la nostra nozione di stato sia un'entità di prima classe e che possiamo avere funzioni di libreria come <*
è molto potente: ho trovato questo particolare idioma davvero utile per attraversare gli alberi, e altri idiomi simili sono stati abbastanza utili per altri codici.
Davvero una buona risposta. Non ero a conoscenza della monade di stato fino a questo punto. Grazie mille! – fuji