Qualcuno può spiegare la funzione trasversale in Haskell?


99

Sto provando e non riesco a recuperare la traversefunzione da Data.Traversable. Non riesco a vedere il suo punto. Dato che provengo da un background imperativo, qualcuno può spiegarmelo in termini di ciclo imperativo? Lo pseudo codice sarebbe molto apprezzato. Grazie.


1
L'articolo The Essence of the Iterator Pattern potrebbe essere utile in quanto costruisce la nozione di traverse passo dopo passo. Tuttavia, sono presenti alcuni concetti avanzati
Jackie

Risposte:


121

traverseè uguale a fmap, tranne per il fatto che consente anche di eseguire effetti mentre si ricostruisce la struttura dei dati.

Dai un'occhiata all'esempio dalla Data.Traversabledocumentazione.

 data Tree a = Empty | Leaf a | Node (Tree a) a (Tree a)

L' Functoristanza di Treesarebbe:

instance Functor Tree where
  fmap f Empty        = Empty
  fmap f (Leaf x)     = Leaf (f x)
  fmap f (Node l k r) = Node (fmap f l) (f k) (fmap f r)

Ricostruisce l'intero albero, applicando fogni valore.

instance Traversable Tree where
    traverse f Empty        = pure Empty
    traverse f (Leaf x)     = Leaf <$> f x
    traverse f (Node l k r) = Node <$> traverse f l <*> f k <*> traverse f r

L' Traversableistanza è quasi la stessa, tranne per il fatto che i costruttori sono chiamati in stile applicativo. Ciò significa che possiamo avere effetti (collaterali) durante la ricostruzione dell'albero. L'applicazione è quasi la stessa delle monadi, tranne per il fatto che gli effetti non possono dipendere dai risultati precedenti. In questo esempio significa che non è possibile fare qualcosa di diverso dal ramo destro di un nodo a seconda dei risultati della ricostruzione del ramo sinistro, ad esempio.

Per ragioni storiche, la Traversableclasse contiene anche una versione monadica di traversechiamata mapM. A tutti gli effetti mapMè lo stesso di traverse: esiste come metodo separato perché Applicativesolo in seguito è diventato una superclasse di Monad.

Se lo implementassi in un linguaggio impuro, fmapsarebbe lo stesso di traverse, poiché non c'è modo di prevenire gli effetti collaterali. Non puoi implementarlo come un ciclo, poiché devi attraversare la struttura dei dati in modo ricorsivo. Ecco un piccolo esempio di come lo farei in Javascript:

Node.prototype.traverse = function (f) {
  return new Node(this.l.traverse(f), f(this.k), this.r.traverse(f));
}

Tuttavia, implementarlo in questo modo ti limita agli effetti consentiti dal linguaggio. Se vuoi il non determinismo (che è l'istanza dell'elenco dei modelli applicativi) e la tua lingua non lo ha integrato, sei sfortunato.


11
Cosa significa il termine "effetto"?
missingfaktor

24
@missingfaktor: Significa le informazioni strutturali di a Functor, la parte che non è parametrica. Il valore dello stato in State, il fallimento in Maybee Either, il numero di elementi in []e, naturalmente, gli effetti collaterali esterni arbitrari in IO. Non mi interessa come termine generico (come le Monoidfunzioni che usano "empty" e "append", il concetto è più generico di quanto il termine suggerisca all'inizio) ma è abbastanza comune e serve allo scopo abbastanza bene.
CA McCann

@ CA McCann: Capito. Grazie per aver risposto!
missingfaktor

1
"Sono abbastanza sicuro che non dovresti farlo [...]." Assolutamente no - sarebbe così brutto come far apdipendere gli effetti dai risultati precedenti. Ho riformulato tale osservazione di conseguenza.
duplode

2
"L'applicazione è quasi la stessa delle monadi, tranne per il fatto che gli effetti non possono dipendere dai risultati precedenti." ... molte cose sono andate a posto per me con questa riga, grazie!
agam

58

traversetrasforma le cose dentro a Traversablein una Traversabledelle cose "dentro" una Applicative, data una funzione che fa Applicatives fuori dalle cose.

Usiamo Maybeas Applicativeed elenchiamo come Traversable. Per prima cosa abbiamo bisogno della funzione di trasformazione:

half x = if even x then Just (x `div` 2) else Nothing

Quindi, se un numero è pari, ne otteniamo la metà (all'interno di a Just), altrimenti lo otteniamo Nothing. Se tutto va "bene", apparirà così:

traverse half [2,4..10]
--Just [1,2,3,4,5]

Ma...

traverse half [1..10]
-- Nothing

Il motivo è che la <*>funzione viene utilizzata per costruire il risultato e quando uno degli argomenti è Nothing, torniamo Nothingindietro.

Un altro esempio:

rep x = replicate x x

Questa funzione genera un elenco di lunghezza xcon il contenuto x, ad esempio rep 3= [3,3,3]. Qual è il risultato traverse rep [1..3]?

Otteniamo i risultati parziali [1], [2,2]e [3,3,3]utilizzare rep. Ora la semantica delle liste così com'è Applicatives"accetta tutte le combinazioni", ad esempio (+) <$> [10,20] <*> [3,4]è [13,14,23,24].

"Tutte le combinazioni" di [1]e [2,2]sono due volte [1,2]. Tutte le combinazioni di due volte [1,2]e [3,3,3]sono sei volte [1,2,3]. Quindi abbiamo:

traverse rep [1..3]
--[[1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3]]

1
Il tuo risultato finale mi ricorda questo .
hugomg

3
@missingno: Sì, hanno persofac n = length $ traverse rep [1..n]
Landei

1
In realtà, è lì sotto "List-encoding-programmer" (ma usando le comprensioni della lista). Questo sito web è completo :)
hugomg

1
@missingno: Hm, non è esattamente la stessa cosa ... entrambi fanno affidamento sul comportamento del prodotto cartesiano della lista monade, ma il sito ne usa solo due alla volta, quindi è più simile a fare liftA2 (,)che il modulo più generico che usa traverse.
CA McCann

41

Penso che sia più facile da capire in termini di sequenceA, come traversepuò essere definito come segue.

traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)
traverse f = sequenceA . fmap f

sequenceA sequenzia insieme gli elementi di una struttura da sinistra a destra, restituendo una struttura con la stessa forma contenente i risultati.

sequenceA :: (Traversable t, Applicative f) => t (f a) -> f (t a)
sequenceA = traverse id

Puoi anche pensare di sequenceAinvertire l'ordine di due funtori, ad esempio passando da un elenco di azioni a un'azione che restituisce un elenco di risultati.

Quindi traverseprende una struttura e si applica fper trasformare ogni elemento della struttura in un applicativo, quindi sequenzia gli effetti di quegli applicativi da sinistra a destra, restituendo una struttura con la stessa forma contenente i risultati.

Puoi anche confrontarlo con Foldable, che definisce la funzione correlata traverse_.

traverse_ :: (Foldable t, Applicative f) => (a -> f b) -> t a -> f ()

Quindi puoi vedere che la differenza fondamentale tra Foldablee Traversableè che quest'ultimo ti consente di preservare la forma della struttura, mentre il primo richiede di piegare il risultato in un altro valore.


Un semplice esempio del suo utilizzo è l'utilizzo di un elenco come struttura attraversabile e IOcome applicativo:

λ> import Data.Traversable
λ> let qs = ["name", "quest", "favorite color"]
λ> traverse (\thing -> putStrLn ("What is your " ++ thing ++ "?") *> getLine) qs
What is your name?
Sir Lancelot
What is your quest?
to seek the holy grail
What is your favorite color?
blue
["Sir Lancelot","to seek the holy grail","blue"]

Sebbene questo esempio sia piuttosto poco entusiasmante, le cose diventano più interessanti quando traverseviene utilizzato su altri tipi di contenitori o utilizzando altri applicativi.


Quindi traverse è semplicemente una forma più generale di mapM? In effetti, sequenceA . fmapper le liste è equivalente a sequence . mapno?
Raskell

Cosa intendi per "sequenziamento degli effetti collaterali"? Qual è "effetto collaterale" nella tua risposta? Ho solo pensato che gli effetti collaterali sono possibili solo nelle monadi. Saluti.
Marek

1
@Marek "Ho solo pensato che gli effetti collaterali sono possibili solo nelle monadi" - La connessione è molto più libera di quella: (1) Il IO tipo può essere usato per esprimere gli effetti collaterali; (2) IOsembra essere una monade, che risulta essere molto conveniente. Le monadi non sono essenzialmente collegate agli effetti collaterali. Va anche notato che c'è un significato di "effetto" che è più ampio di "effetto collaterale" nel suo senso usuale - uno che include calcoli puri. Su quest'ultimo punto, vedi anche Cosa significa esattamente "efficace" .
duplode

(A proposito, @hammar, mi sono preso la libertà di cambiare "effetto collaterale" in "effetto" in questa risposta per i motivi delineati nel commento sopra.)
duplode

17

È un po 'come fmap, tranne per il fatto che puoi eseguire effetti all'interno della funzione mapper, che cambia anche il tipo di risultato.

Immaginate un elenco di numeri interi che rappresentano ID utente in un database: [1, 2, 3]. Se vuoi fmapquesti ID utente per i nomi utente, non puoi usare un tradizionale fmap, perché all'interno della funzione devi accedere al database per leggere i nomi utente (che richiede un effetto - in questo caso, usando la IOmonade).

La firma di traverseè:

traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)

Con traverse, puoi fare effetti, quindi, il tuo codice per mappare gli ID utente ai nomi utente è simile a:

mapUserIDsToUsernames :: (Num -> IO String) -> [Num] -> IO [String]
mapUserIDsToUsernames fn ids = traverse fn ids

C'è anche una funzione chiamata mapM:

mapM :: (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b)

Qualsiasi uso di mapMpuò essere sostituito con traverse, ma non il contrario. mapMfunziona solo per le monadi, mentre traverseè più generico.

Se vuoi solo ottenere un effetto e non restituire alcun valore utile, ci sono traverse_e mapM_versioni di queste funzioni, entrambe ignorano il valore restituito dalla funzione e sono leggermente più veloci.



7

traverse è il ciclo. La sua implementazione dipende dalla struttura dati da attraversare. Questo potrebbe essere un elenco, albero, Maybe, Seq(influenza), o qualsiasi cosa che ha un modo generico di essere attraversato tramite qualcosa di simile a un ciclo for o di una funzione ricorsiva. Un array avrebbe un ciclo for, una lista un ciclo while, un albero o qualcosa di ricorsivo o la combinazione di uno stack con un ciclo while; ma nei linguaggi funzionali non hai bisogno di questi ingombranti comandi di ciclo: combini la parte interna del ciclo (a forma di funzione) con la struttura dei dati in modo più diretto e meno prolisso.

Con la Traversabletypeclass, potresti probabilmente scrivere i tuoi algoritmi in modo più indipendente e versatile. Ma la mia esperienza dice che di Traversablesolito viene utilizzato solo per incollare semplicemente algoritmi a strutture di dati esistenti. È abbastanza carino non dover scrivere funzioni simili anche per diversi tipi di dati qualificati.

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.