21 - nested comprehension és flatMap

Különösen sokat tud lendíteni kódunk olvashatóságán, ha ,,egymásba ágyazott ciklusokat'' hajtanánk végre, azaz pl. ha több kollekciót is bejárnánk. Ennek a szintaxisa: a for comprehension belsejében az enumerátorokat pontosvesszőkkel választjuk el.

nested foreach

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
for(
  i <- 1 to 10;
  j <- 1 to i
) println( s"$i * $j = ${ i*j }" ) //string interpoláció 

/* output:
1 * 1 = 1
2 * 1 = 2
2 * 2 = 4
3 * 1 = 3
3 * 2 = 6
3 * 3 = 9
4 * 1 = 4
...
*/

Pontosvesszővel szépen egymás mellett elválasztott enumerátorokból egy beágyazott foreach hívás lesz, általában a

1
for( x1 <- c1; x2 <- c2; ...; xn <- cn ) E

for comprehensionből a Scala fordító a

1
c1.foreach( { x1 => c2.foreach( { x2 => ... cn.foreach( { xn => E } )})})

kifejezést generálja.

  • Mi lesz ennek a for kifejezésnek a típusa? Értéke?
A típusa `Unit` lesz, az értéke `()`. Válasz mutatása
  • Mire fordul például a fenti példa comprehension?
1
(1 to 10).foreach( { i => (1 to i).foreach( { j => println( s"$i * $j = ${ i*j }" ) } ) } )
Válasz mutatása

nested map?

A for yield comprehensionba is lehet pontosvesszővel több enumerátort írni.

  • Ha van két string listánk, egyikben pl a színek: "Tök", "Makk" stb, másikban a számok: "hetes", "alsó", "ász" stb, hogy hozunk létre egy listát ez alapján, melyben a "Tök hetes", "Makk alsó" stb. stringek szerepelnek, minden szín-szám kombináció pont egyszer?
1
2
3
4
5
6
val szinek = List("Tök", "Zöld", "Makk", "Piros")
val szamok = List("hetes", "nyolcas", "kilences", "tizes", "alsó", "felső", "király", "ász")

val cards = for( szin <- szinek; szam <- szamok ) yield s"$szin $szam"

println( cards ) //List(Tök hetes, Tök nyolcas, ...)
Válasz mutatása
  • Vajon ez a fenti példakód comprehension miért nem lehet szinek.map( { szin => szamok.map( { szam => s"$szin $szam" } ) } )?
Mert nem jön ki a típus! Próbáljuk ki! Az eredmény
1
2
3
4
5
6
List(
  List(Tök hetes, Tök nyolcas, Tök kilences, Tök tizes, Tök alsó, Tök felső, Tök király, Tök ász),
  List(Zöld hetes, Zöld nyolcas, Zöld kilences, Zöld tizes, Zöld alsó, Zöld felső, Zöld király, Zöld ász),
  List(Makk hetes, Makk nyolcas, Makk kilences, Makk tizes, Makk alsó, Makk felső, Makk király, Makk ász),
  List(Piros hetes, Piros nyolcas, Piros kilences, Piros tizes, Piros alsó, Piros felső, Piros király, Piros ász)
  )
lesz, típusa `List[List[String]]`. Válasz mutatása

Mi történik valójában...

... mikor egy listán ható több enumerátoros for yield comprehensiont írunk?

Ismerkedjünk meg a List[T] osztály néhány újabb metódusával:

konkatenálás

:::(that: List[T): List[T] egymás után illeszti, ,,összeragasztja'' (avagy konkatenálja) a két listát, pl. List(1,4) ::: List(2,8) = List(1,4,2,8).

Vegyük észre, hogy ha ehelyett a :: operátort használnánk, akkor a List(List(1,4), 2, 8)-at kapnánk (ennek a típusáról és hogy miért fordul egyáltalán, később még lesz szó), egy háromelemű listát; ha pedig ezeket a listákat egymásba ágyazott mapok értékeként kapnánk, pl. a List(1,2).map( { i => List(1,4).map( { j => i*j } ) } ) kifejezéssel, akkor mit is kapnánk?

1
List(List(1, 4), List(2, 8))
Válasz mutatása

flatten

flatten: List[List[T]]-ből List[T]-t készít, ,,feloldva'' az eggyel lentebbi szintű listákat. (Erről később még lesz szó pontosabban)

Tehát vajon pl. az előző List(List(1,4), List(2,8))-on hívva a flatten metódust, mi lesz a kapott érték?

1
List(1,4,2,8)
Válasz mutatása

Vajon mi történik, ha a List(List(1,4), 2, 8).flatten kifejezést próbáljuk kiértékelni?

Így nem fordul le: a `2` és a `8` elemek nem listák. Ha `List(List(1,4),List(2),List(8)).flatten` lenne a hívás, abból `List(1,4,2,8)` készülne. Válasz mutatása

flatMap

flatMap[U]( f: T => List[U] ): List[U]: a lista minden elemén kiértékeli az f metódust, a kapott List[U]-kat pedig konkatenálja, így lesz az érték típusa ismét List[U].

feladatok

Implementáljunk a saját listánkba flatMap metódust, felhasználva a ::: metódust!

1
2
3
4
5
6
7
8
9
trait MyList[T] { //map, :::, filter, foreach, stb mellé még pluszban:
  def flatMap[U]( f: T => List[U] ): List[U]
}
case class MyNil[T]() extends MyList[T] {
  override def flatMap[U]( f: T => List[U] ) = MyNil() //lehetne egyébként this is
}
case class MyCons[T](head: T, tail: MyList[T]) extends MyList[T] {
  override def flatMap[U]( f: T => List[U] ) = f(head) ::: (tail.flatMap(f)) //igaz, így nem tail recursive
}
Válasz mutatása

Implementáljunk a saját listánkba flatMap metódust, felhasználva a map és flatten metódusokat!

1
2
3
trait MyList[T] { //map, flatten, :::, filter, foreach, stb mellé még pluszban:
  def flatMap[U]( f: T => List[U] ): List[U] = map(f).flatten  //ezt lehet már a traitbe is tenni, mindig ezt kell csinálni
}

Válasz mutatása

Implementáljunk a saját listánkba map metódust, felhaszválva a flatMap metódust és az egyelemű listát létrehozó konstruktort!

1
2
3
4
5
6
7
8
trait MyList[T] { //map, flatten, :::, filter, foreach, stb mellé még pluszban:
  def map[U]( f: T => U ): List[U] = flatMap( { x => MyList(f(x)) } )
  //pl. ha az (1,4,2) listán mapjük az {x => 2*x} függvényt:
  //1-ből lesz MyList(2*1), azaz MyList(2),
  //4-ből MyList(8),
  //2-ből MyList(4),
  //a flatMap pedig ezeket konkatenálva visszaadja a MyList(2,8,4)-et, ahogy akartuk
}

Válasz mutatása

Note: ,,egyelemű listát létrehozó konstruktort'' még nem írtunk, a companion objecteknél fogjuk látni, hogy hogyan lehetséges.

Hogyan lehet pl. flattent készíteni a flatMap használatával egy List[List[T]] típusú list listán?

1
list.flatMap( { x => x } )

Válasz mutatása

nested yield: flatMapek és egy map

Láthatjuk tehát, hogy a flatMap, flatten és map metódusok, meg az egyelemű konstruktor, ami egy T típusból készít egy List[T]-t, szépen kifejezhetőek egymásból.

A flatMap pedig onnan került elő, hogy amikor a Scala fordító egy for ( x1 <- c1; x2 <- c2; ...; xn <- cn ) yield E alakú kifejezést lát, azt a következőre írja át:

1
c1.flatMap( x1 => c2.flatMap( x2 => ... => cn.map( xn => E ) ))

azaz: a legutolsó enumerátoron mapet, az összes többin pedig flatMapet használ!

Lássuk be, hogy ez tényleg működik: értékeljük ki a for( i <- List(1,2); j <- List(1,4) ) yield i*j kifejezést! Mi a típusa?

Átírva flatMap-re ezt kapjuk:

1
List(1,2).flatMap( { i => List(1,4).map( { j => i*j} ) } )

Ha mondjuk úgy közelítjük meg az átírást, hogy előbb végrehajtjuk a flatMap belsejében lévő függvényt 1-re is és 2-re is, akkor az elsőből ezt kapjuk:

1
List(1,4).map( { j => 1*j } )

amit ha elvégzünk mindkét elemére az (1,4) listának és az eredményeket listába tesszük, kapjuk az (1,4) listát, a 2-re pedig:

1
2
3
List(1,4).map( { j => 2*j } ) //a külső enumerátor bejárja az (1,2) listát, i helyére ezek kerülnek, most 2
=
List( 2*1, 2*4 ) = List(2,8)

A flatMap pedig konkatenálja a két eredményt és azt kapjuk, hogy

1
List(1,4,2,8)

ahogy tényleg láttuk is az első példánkon. Típusa List[Int].

Válasz mutatása

1
//TODO gyakorlat: dantézás, háromszorozzuk meg a betűket sanyi => sssaaannnyyyiii, először 345, 291, 363, 315, aztán KELL HOZZÁ FOLD

Utolsó frissítés: 2020-12-24 17:03:27