19 - foreach és for comprehension

A List[T] osztály rendelkezik még egy szintén generikus foreach metódussal:

1
List[T].foreach[U]( f: T=>U ): Unit

ami kap egy függvényt, ami inputként a lista elemeinek típusát várja, outputként meg tetszőleges típust visszaadhat (ezért generikus az U paraméterben), kiértékeli az f függvényt a lista összes elemén, az eredményeket pedig eldobja. Nagyon hasonlót implementáltunk már, mikor for ciklust írtunk.

  • Hogy írnánk meg a foreach metódust a saját lista implementációnkon?
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
trait List[T] {
  def foreach[U]( f: T=>U ): Unit        //a traitben kell definiáljuk, hogy forduljon
}
case class Nil[T]() extends List[T] {
  override def foreach[U]( f: T=>U ) = ()  //üres listán nem csinálunk semmit, visszaadjuk a () Unitot
}
case class ::[T](head: T, tail: List[T]) extends List[T] {
  override def foreach[U]( f: T=>U ) = {
    f(head)          //először kiértékeljük f-et a fejelemen
    tail.foreach(f)  //aztán rekurzívan a többi elemen is
  }
}
Válasz mutatása
  • Hogy íratnánk ki egy list lista elemeit szóközzel elválasztva? Az utolsó elem után tehetünk plusz egy spacet.
1
2
3
val list = List(1, 4, 2, 8, 5, 7)

list.foreach( { x => print(x + " " ) } ) //prints '1 4 2 8 5 7 '
Válasz mutatása

Ez azt is jelenti, hogy a paraméterként átadott függvényt a mellékhatása miatt hívjuk meg, hiszen az érték ,,eltűnik'', az eredmény mindenképp () lesz, a Unit típus egyetlen eleme.

string interpoláció, toString

Az előző feladat list.foreach( { x => print( x + " " ) } ) mintamegoldásában esetleg szöget üthetett valakinek a fejébe, hogy ez most miért is fordul le egyáltalán? A list előtte List[Int]ként van deklarálva, ezek szerint lehet Intet és Stringet összeadni, majd azt kiíratni?

  • Egyrészt igen, a https://www.scala-lang.org/api/current/scala/Int.html doksiját megnézve láthatjuk, hogy szerepel az Int osztályban a +(x: String): String metódus, ami ahogy korábban láthattuk, perfectly fine deklaráció, ezért fordul le pl. a 7 + "fő" kifejezés, és lesz az értéke a "7fő" string. Ugyanakkor, a doksi azt is mondja, hogy 2.13.0 óra ez deprecated, azaz más módszer ajánlott helyette, mégpedig a string interpoláció:

    • ha egy "" macskakörmök közti string elé az s karaktert írjuk, akkor a benne lévő $ jelekkel változókat, sőt tetszőleges kifejezéseket írhatunk
    • pl. a fenti esetben s"$x " rendben van, ez így egy interpolált string, avagy processed string literal
    • a dollárt ha változónév követi, akkor ennek helyére a változónak az értéke helyettesítődik be
    • ha konkrétan a dollár jelre van szükségünk, akkor két dollárt írva megkapjuk a dollár karaktert
    • ha a dollár után kapcsosba teszünk egy tetszőleges kifejezést, azt kiértékeli a scala runtime és az eredmény kerül behelyettesítésre.

A string interpoláció is sokkal olvashatóbbá tud tenni egy kódot, érdemes használni, létezik még az f és a raw mint beépített interpolátorok az s mellett, érdemes lehet átfutni a doksiját.

  • Másrészt, a print(x) is lefordulna, bármilyen típusú is x. Ennek oka az, hogy

    • minden Scala objektumnak (a Javahoz hasonlóan) van egy toString: String metódusa
    • a print metódusnak van egy print( s: String ) változata és egy print( a: Any ) változata is (az Any osztályról hamarosan lesz szó)
    • a print(a: Any) metódus pedig mindösszesen annyit csinál, hogy meghívja a print( a.toString )-et
    • korábban mikor a case classainkat kiírattuk, azért néztek ki olyan olvashatóan, mert a case classokhoz a fordító generál egy megfelelő override toString metódust automatikusan, az osztály neve, nyitójel, mezők neve, egyenlő, értékek toStringjei vesszőkkel elválasztva, csukójel formátumban, mint pl. a Pont(x=3,y=2) is volt
    • persze ha mi is felülírjuk a toStringet, akkor a fordító nem generál automatikusan, csak ha nem tesszük.

for comprehension

Ahogy Javában is be lehet járni a kollekciókat elemenként, a Scalás listának is van ún. ,,for comprehension''-je, melyet szintaktikailag így írhatunk fel:

1
2
3
4
5
val list = List(1, 4, 2, 8, 5, 7)

for ( x <- list ) {  //persze ehhez most nem feltétlen kell a kapcsos
  print( s"$x " )   //prints '1 4 2 8 5 7 '
}

Tehát úgy általában a listára alkalmazott for comprehension szintaxisa: for (, egy azonosító, amivel a lista ,,általános'' elemét fogjuk a magban hivatkozni, példában most x, majd egy <- operátor, amit a listánk követ, ) és végül egy kifejezés, melyben jó, ha szerepel is a listaelem azonosítónk, most az x.

Ha valaki belenézett a gyakorlatos tesztesetekbe, akkor láthatott benne for( n <- 1 to 100 ) ... alakú részeket, ez nagyon hasonlít ehhez a fentihez, és tényleg, a lenti is valid Scala kód:

1
for( n <- 1 to 10 ) print( s"$n " ) //prints '1 2 3 4 5 6 7 8 9 10 '

Ez már egészen hasonlít egy for ciklusra (legalábbis egy olyanra, melyben a ciklusváltozót nem módosítjuk a ciklusmagban).

A for comprehensionnel kapcsolatban az a valóság, hogy ha a Scala fordító egy for ( x <- c ) E alakú kifejezést talál, akkor azt automatikusan átalakítja c.foreach( { x => E } ) alakúra. Tehát például ez a két kód teljesen ugyanaz:

1
2
3
for( x <- list ) print( s"$x " )

list.foreach( { x => print(s"$x ") } )

1 to 10: a Range osztály

Nincs ez másképp a for ( n <- 1 to 10 ) E alakú kifejezéssel sem. Mire is kellene annak fordulnia ezek szerint?

1
(1 to 10).foreach( { n => E } )
Válasz mutatása

Erre is fordul! Nem arról van szó, hogy az 1 to 10 valamiféle spéci nyelvi elem lenne. Arról van szó, hogy az Int osztálynak van egy to(m: Int): Range metódusa, ami visszaad egy Range típusú objektumot, melynek van foreach metódusa, ami az intervallum összes elemén kiértékeli az f függvényt.

Hogyan implementálnánk egy hasonló osztályt?

Egy lehetséges megoldás:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
case class MyInt(n: Int) {  //lehet saját Intünk is, mindegy
  def to(m: Int) = Range(n,m)
}

case class Range(n: Int, m: Int) {
  def foreach[U]( f: T=>U ): Unit = 
    if( n<=m ) {
      f(n)                         // kiértékeljük f-et az intervallum első elemén
      Range(n+1,m).foreach(f)      // majd egy eggyel ,,később kezdődő'' intervallumon foreach
    } else () //ez nem szükséges
}
Persze nem pont így van implementálva a Scala libraryben, amit ebből meg kell jegyeznünk, hogy a `to` sem beépített kulcsszó, hanem egy `Int` metódus, ami egy `Range` objektummal tér vissza, ami egy bejárható intervallumot reprezentál, ezért fordul a `for` comprehension, ha `for ( n <- 1 to 10 )` formában hívjuk. Válasz mutatása

saját osztályunkban foreach grants for comprehension

A foreach nem egy ad hoc Scala név: a legtöbb programozási nyelvben így hívják a kollekciókat bejáró metódust. Annak, hogy Scalában a fordító egy for comprehensiont foreach hívássá konvertál, majd az átalakított változatot fordítja le, az a következménye, hogy a saját osztályainkban is ha deklarálunk egy def foreach[U]( f: T => U ): Unit metódust, ahol mondjuk T is valamiféle generikus típus, akkor használhatunk rajta for comprehensiont for free.

Tehát:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
trait MyList[T] {
  def foreach[U](f: T=>U): Unit
}
case class MyNil[T]() extends MyList[T] {
  override def foreach[U]( f: T=>U ) = ()
}
case class MyCons[T](head: T, tail: MyList[T]) extends MyList[T] {
  override def foreach[U]( f: T=>U ) = {
    f(head)
    tail.foreach(f)
  }
}

val myList = MyCons(1, MyCons(4, MyCons(8, MyNil() ) ) )

for( x <- myList ) print(s"$x ") //prints '1 4 8 '

fordul és működik, sokkal olvashatóbbá téve sok esetben a kódunkat.

Kérdések, feladatok


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