Confuso con la trasformazione da comprensione a flatMap / Map


87

Non mi sembra davvero di capire Map e FlatMap. Quello che non riesco a capire è come una per comprensione sia una sequenza di chiamate annidate a map e flatMap. Il seguente esempio è tratto da Functional Programming in Scala

def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = for {
            f <- mkMatcher(pat)
            g <- mkMatcher(pat2)
 } yield f(s) && g(s)

si traduce in

def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = 
         mkMatcher(pat) flatMap (f => 
         mkMatcher(pat2) map (g => f(s) && g(s)))

Il metodo mkMatcher è definito come segue:

  def mkMatcher(pat:String):Option[String => Boolean] = 
             pattern(pat) map (p => (s:String) => p.matcher(s).matches)

E il metodo del modello è il seguente:

import java.util.regex._

def pattern(s:String):Option[Pattern] = 
  try {
        Some(Pattern.compile(s))
   }catch{
       case e: PatternSyntaxException => None
   }

Sarebbe fantastico se qualcuno potesse far luce sulla logica alla base dell'utilizzo di map e flatMap qui.

Risposte:


201

TL; DR vai direttamente all'esempio finale

Proverò a ricapitolare.

Definizioni

La forcomprensione è una scorciatoia di sintassi da combinare flatMape mapin un modo che è facile da leggere e da ragionare.

Semplifichiamo un po 'le cose e supponiamo che ogni classmetodo che fornisce entrambi i metodi sopra menzionati possa essere chiamato a monade useremo il simbolo M[A]per indicare a monadcon un tipo interno A.

Esempi

Alcune monadi comunemente viste includono:

  • List[String] dove
    • M[X] = List[X]
    • A = String
  • Option[Int] dove
    • M[X] = Option[X]
    • A = Int
  • Future[String => Boolean] dove
    • M[X] = Future[X]
    • A = (String => Boolean)

mappa e flatMap

Definito in una monade generica M[A]

 /* applies a transformation of the monad "content" mantaining the 
  * monad "external shape"  
  * i.e. a List remains a List and an Option remains an Option 
  * but the inner type changes
  */
  def map(f: A => B): M[B] 

 /* applies a transformation of the monad "content" by composing
  * this monad with an operation resulting in another monad instance 
  * of the same type
  */
  def flatMap(f: A => M[B]): M[B]

per esempio

  val list = List("neo", "smith", "trinity")

  //converts each character of the string to its corresponding code
  val f: String => List[Int] = s => s.map(_.toInt).toList 

  list map f
  >> List(List(110, 101, 111), List(115, 109, 105, 116, 104), List(116, 114, 105, 110, 105, 116, 121))

  list flatMap f
  >> List(110, 101, 111, 115, 109, 105, 116, 104, 116, 114, 105, 110, 105, 116, 121)

per espressione

  1. Ogni riga dell'espressione che utilizza il <-simbolo viene tradotta in una flatMapchiamata, ad eccezione dell'ultima riga che viene tradotta in una mapchiamata conclusiva , dove il "simbolo vincolato" sul lato sinistro viene passato come parametro alla funzione argomento (cosa abbiamo chiamato in precedenza f: A => M[B]):

    // The following ...
    for {
      bound <- list
      out <- f(bound)
    } yield out
    
    // ... is translated by the Scala compiler as ...
    list.flatMap { bound =>
      f(bound).map { out =>
        out
      }
    }
    
    // ... which can be simplified as ...
    list.flatMap { bound =>
      f(bound)
    }
    
    // ... which is just another way of writing:
    list flatMap f
    
  2. Un'espressione for con una sola <-viene convertita in una mapchiamata con l'espressione passata come argomento:

    // The following ...
    for {
      bound <- list
    } yield f(bound)
    
    // ... is translated by the Scala compiler as ...
    list.map { bound =>
      f(bound)
    }
    
    // ... which is just another way of writing:
    list map f
    

Adesso al punto

Come puoi vedere, l' mapoperazione preserva la "forma" dell'originale monad, quindi lo stesso accade per l' yieldespressione: a Listrimane a Listcon il contenuto trasformato dall'operazione in yield.

D'altra parte ogni linea di rilegatura in forè solo una composizione di successive monads, che devono essere "appiattite" per mantenere un'unica "forma esterna".

Supponiamo per un momento che ogni rilegatura interna sia stata tradotta in una mapchiamata, ma la mano destra fosse la stessa A => M[B]funzione, finiresti con un M[M[B]]per ogni riga nella comprensione.
L'intento dell'intera forsintassi è di "appiattire" facilmente la concatenazione di operazioni monadiche successive (cioè operazioni che "elevano" un valore in una "forma monadica" :) A => M[B], con l'aggiunta di un'operazione finale mapche possibilmente esegue una trasformazione conclusiva.

Spero che questo spieghi la logica alla base della scelta della traduzione, che viene applicata in modo meccanico, ovvero: n flatMapchiamate annidate concluse da una singola mapchiamata.

Un esempio illustrativo artificioso
inteso a mostrare l'espressività della forsintassi

case class Customer(value: Int)
case class Consultant(portfolio: List[Customer])
case class Branch(consultants: List[Consultant])
case class Company(branches: List[Branch])

def getCompanyValue(company: Company): Int = {

  val valuesList = for {
    branch     <- company.branches
    consultant <- branch.consultants
    customer   <- consultant.portfolio
  } yield (customer.value)

  valuesList reduce (_ + _)
}

Riuscite a indovinare il tipo di valuesList?

Come già detto, la forma del monadviene mantenuta attraverso la comprensione, quindi iniziamo con a Listin company.branchese dobbiamo terminare con a List.
Il tipo interno invece cambia ed è determinato yielddall'espressione: che ècustomer.value: Int

valueList dovrebbe essere un file List[Int]


1
Le parole "è uguale a" appartengono al meta-linguaggio e dovrebbero essere spostate fuori dal blocco di codice.
giorno

3
Ogni principiante FP dovrebbe leggere questo. Come si può ottenere questo risultato?
mert inan

1
@melston Facciamo un esempio con Lists. Se si mapesegue due volte una funzione A => List[B](che è una delle operazioni monadiche essenziali) su un valore, si finisce con un List [List [B]] (stiamo dando per scontato che i tipi corrispondano). Il ciclo interno per la comprensione compone queste funzioni con l' flatMapoperazione corrispondente , "appiattendo" la forma List [List [B]] in un semplice List [B] ... spero che sia chiaro
pagoda_5b

1
è stata pura meraviglia leggere la tua risposta. Vorrei che scrivessi un libro su scala, hai un blog o qualcosa del genere?
Tomer Ben David

1
@coolbreeze Potrebbe essere che non l'ho espresso chiaramente. Quello che volevo dire è che la yieldclausola è customer.value, il cui tipo è Int, quindi l'intero for comprehensionrestituisce a List[Int].
pagoda_5b

7

Non sono una mente da scala mega, quindi sentiti libero di correggermi, ma è così che mi spiego la flatMap/map/for-comprehensionsaga!

Per capire for comprehensione la sua traduzione scala's map / flatMapdobbiamo fare piccoli passi e capire le parti che compongono - mape flatMap. Ma non è scala's flatMapsolo mapcon flattente chiediti! se è così, perché così tanti sviluppatori trovano così difficile capirlo o capirlo for-comprehension / flatMap / map. Bene, se guardi solo la scala mape la flatMapfirma, vedi che restituiscono lo stesso tipo di ritorno M[B]e lavorano sullo stesso argomento di input A(almeno la prima parte della funzione che prendono) se è così, cosa fa la differenza?

Il nostro piano

  1. Comprendi le scale map.
  2. Comprendi le scale flatMap.
  3. Comprendere le scale for comprehension.`

Mappa di Scala

firma della scala mappa:

map[B](f: (A) => B): M[B]

Ma c'è una parte importante che manca quando guardiamo questa firma, ed è - da dove Aviene? il nostro contenitore è di tipo, Aquindi è importante esaminare questa funzione nel contesto del contenitore - M[A]. Il nostro contenitore potrebbe essere un Listdi elementi di tipo Ae la nostra mapfunzione prende una funzione che trasforma ogni elemento di tipo Ain tipo B, quindi restituisce un contenitore di tipo B(o M[B])

Scriviamo la firma della mappa tenendo conto del contenitore:

M[A]: // We are in M[A] context.
    map[B](f: (A) => B): M[B] // map takes a function which knows to transform A to B and then it bundles them in M[B]

Nota un fatto estremamente molto importante sulla mappa : si raggruppa automaticamente nel contenitore di output su M[B]cui non hai alcun controllo. Sottolineiamo di nuovo:

  1. mapsceglie il contenitore di output per noi e sarà lo stesso contenitore della fonte su cui lavoriamo, quindi per M[A]contenitore otteniamo lo stesso Mcontenitore solo per B M[B]e nient'altro!
  2. mapfa questa containerizzazione per noi diamo solo una mappatura da Aa Be la metterebbe nella scatola di M[B]la metterà nella scatola per noi!

Vedi che non hai specificato come containerizeall'oggetto hai appena specificato come trasformare gli oggetti interni. E poiché abbiamo lo stesso contenitore Mper entrambi M[A]e M[B]questo significa che M[B]è lo stesso contenitore, il che significa che se lo hai, List[A]ne avrai uno List[B]e, cosa più importante, maplo farai per te!

Ora che ci siamo occupati mappassiamo a flatMap.

FlatMap di Scala

Vediamo la sua firma:

flatMap[B](f: (A) => M[B]): M[B] // we need to show it how to containerize the A into M[B]

Vedi la grande differenza da map a flatMapin flatMap, gli stiamo fornendo la funzione che non si limita a convertire da A to Bma anche a containerizzarlo in M[B].

perché ci interessa chi fa la containerizzazione?

Allora perché ci preoccupiamo così tanto della funzione di input per map / flatMap la containerizzazione M[B]o la mappa stessa esegue la containerizzazione per noi?

Vedete nel contesto di for comprehensionciò che sta accadendo sono molteplici trasformazioni sull'articolo fornito nel, forquindi stiamo dando al prossimo lavoratore nella nostra catena di montaggio la capacità di determinare l'imballaggio. immagina di avere una catena di montaggio in cui ogni operaio fa qualcosa al prodotto e solo l'ultimo operaio lo confeziona in un contenitore! benvenuto a flatMapquesto è il suo scopo, in mapogni lavoratore quando ha finito di lavorare sull'articolo lo impacchetta in modo da ottenere contenitori su contenitori.

Il potente per la comprensione

Ora esaminiamo la tua comprensione tenendo conto di ciò che abbiamo detto sopra:

def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = for {
    f <- mkMatcher(pat)   
    g <- mkMatcher(pat2)
} yield f(s) && g(s)

Cosa abbiamo qui:

  1. mkMatcherrestituisce un containercontenitore che contiene una funzione:String => Boolean
  2. Le regole sono le se abbiamo più <-che traducono flatMaptranne l'ultimo.
  3. Poiché per f <- mkMatcher(pat)prima cosa sequence(pensa assembly line) tutto ciò che vogliamo è prenderlo fe passarlo al prossimo lavoratore nella catena di montaggio, lasciamo al prossimo lavoratore nella nostra catena di montaggio (la funzione successiva) la capacità di determinare quale sarebbe il imballaggio indietro del nostro articolo ecco perché l'ultima funzione è map.
  4. L'ultimo g <- mkMatcher(pat2)lo userà mapperché è l'ultimo nella catena di montaggio! quindi può fare solo l'operazione finale con la map( g =>quale sì! tira fuori ge utilizza il fche è già stato estratto dal contenitore dal flatMapquindi ci ritroviamo per primi:

    mkMatcher (pat) flatMap (f // estrae la funzione f dai l'oggetto al prossimo lavoratore della catena di montaggio (vedi che ha accesso fe non impacchettarlo indietro, voglio dire lascia che la mappa determini l'imballaggio lascia che il prossimo lavoratore della catena di montaggio determini container. mkMatcher (pat2) map (g => f (s) ...)) // poiché questa è l'ultima funzione nella catena di montaggio, useremo map e tireremo g fuori dal contenitore e nella confezione indietro , la sua mape questa confezione rallenterà completamente e sarà il nostro pacchetto o il nostro contenitore, yah!


4

La logica è concatenare operazioni monadiche che forniscono come vantaggio una corretta gestione degli errori "fail fast".

In realtà è piuttosto semplice. Il mkMatchermetodo restituisce un Option(che è una Monade). Il risultato mkMatcherdell'operazione monadica è a Noneo a Some(x).

L'applicazione della funzione mapo flatMapa a Nonerestituisce sempre a None: la funzione passata come parametro mape flatMapnon viene valutata.

Quindi, nel tuo esempio, se mkMatcher(pat)restituisce None, la flatMap applicata restituirà a None(la seconda operazione monadica mkMatcher(pat2)non verrà eseguita) e la finale maprestituirà nuovamente a None. In altre parole, se una qualsiasi delle operazioni nella comprensione restituisce Nessuno, si ha un comportamento rapido e il resto delle operazioni non vengono eseguite.

Questo è lo stile monadico di gestione degli errori. Lo stile imperativo utilizza eccezioni, che sono fondamentalmente salti (a una clausola catch)

Una nota finale: la patternsfunzioneèun modo tipico di "tradurre" una gestione degli errori di stile imperativo ( try... catch) in una gestione degli errori di stile monadico usandoOption


Sapete perché flatMap(e non map) si usa per "concatenare" la prima e la seconda invocazione mkMatcher, ma perché map(e non flatMap) si usa "concatenare" la seconda mkMatchere il yieldsblocco?
Malte Schwerhoff

1
flatMapsi aspetta che tu passi una funzione che restituisca il risultato "avvolto" / sollevato nella Monade, mentre mapfarà il wrapping / lift da solo. Durante il concatenamento di chiamate di operazioni in for comprehensionè necessario in flatmapmodo che le funzioni passate come parametro possano essere restituite None(non è possibile elevare il valore a Nessuno). L'ultima chiamata di operazione, quella in yielddovrebbe essere eseguita e restituire un valore; un mapa catena che ultima operazione è sufficiente, evita di dover sollevare il risultato della funzione nella monade.
Bruno Grieder

1

Questo può essere tradotto come:

def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = for {
    f <- mkMatcher(pat)  // for every element from this [list, array,tuple]
    g <- mkMatcher(pat2) // iterate through every iteration of pat
} yield f(s) && g(s)

Eseguilo per una migliore visione di come è espanso

def match items(pat:List[Int] ,pat2:List[Char]):Unit = for {
        f <- pat
        g <- pat2
} println(f +"->"+g)

bothMatch( (1 to 9).toList, ('a' to 'i').toList)

i risultati sono:

1 -> a
1 -> b
1 -> c
...
2 -> a
2 -> b
...

Questo è simile a flatMap- loop attraverso ogni elemento in pate foreach element mapit to each element inpat2


0

In primo luogo, mkMatcherrestituisce una funzione la cui firma è String => Boolean, che è una normale procedura java che viene eseguita Pattern.compile(string), come mostrato nella patternfunzione. Quindi, guarda questa riga

pattern(pat) map (p => (s:String) => p.matcher(s).matches)

La mapfunzione viene applicata al risultato di pattern, che è Option[Pattern], quindi pin p => xxxè solo il modello che hai compilato. Quindi, dato un modello p, viene costruita una nuova funzione, che accetta una stringa se controlla se scorrisponde al modello.

(s: String) => p.matcher(s).matches

Nota, la pvariabile è limitata al modello compilato. Ora, è chiaro come String => Booleanviene costruita una funzione con firma mkMatcher.

Successivamente, controlliamo la bothMatchfunzione, che si basa su mkMatcher. Per mostrare come bothMathchfunziona, guardiamo prima a questa parte:

mkMatcher(pat2) map (g => f(s) && g(s))

Poiché abbiamo ottenuto una funzione con firma String => Booleanfrom mkMatcher, che gin questo contesto g(s)è equivalente a Pattern.compile(pat2).macher(s).matches, che restituisce se String s corrisponde a pattern pat2. Allora che ne dici f(s), è lo stesso g(s), l'unica differenza è che, la prima chiamata di mkMatcherusi flatMap, invece di map, perché? Poiché mkMatcher(pat2) map (g => ....)restituisce Option[Boolean], otterrai un risultato annidato Option[Option[Boolean]]se usi mapper entrambe le chiamate, non è quello che vuoi.

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.