Helsingin yliopisto / tietojenkäsittelytieteen laitos / Ohjelmointitekniikka (Scala) / © Arto Wikla 2016

20 Hyvin lyhyesti abstrakteista jäsenistä ja laiskoista funktio-olioista

Muutettu viimeksi 19.4.2016 / Sivu luotu 10.4.2014 / [oppikirjan esimerkit] / [Scala]

Oppikirjan luku 20 esittelee laveasti mm. abstrakteja luokkamäärittelyn jäseniä, funktio-olioiden laiskaa käynnistämistä ja paljon muuta.

Tällä kurssilla vain kurkistetaan muutamaan kiinnostavaan ideaan.

... abstraktia ...

Luokkaan tai piirretyyppiin voidaan määritellä metodien lisäksi myös kenttiä ja tyyppejä(!) abstrakteiksi:

  trait Abstract {
    type T
    def transform(x: T): T
    val initial: T
    var current: T
  }
Tuollainen piirretyyppi voidaan liittää konkreettiseksi tarkoitettuun luokkaan korvaamalla kaikki abstraktit jäsenet konkreettisilla ja kiinnittämällä abstrakti tyyppi:
  class Concrete extends Abstract {
    type T = String
    def transform(x: T) = x + x  // tai def transform(x: String) = x + x
    val initial = "hi"
    var current = initial
  }

val a = new Concrete
println(a.initial + " " + a.transform(a.current)) // hi hihi

Huomioitavaa:

... laiskaa ...

Scalassa val-kentät ja muuttujat voidaan evaluoida (laskea, suorittaa) myös laiskasti, so. vasta sitten, kun niihin viitataan. Normaalisti kenttään asetettava arvo lasketaan samalla kun kenttä määritellään.

Kuten muistetaan, myös funktiot ovat first-class-arvoja – niitäkin voi sijoitella kenttiin tai muuttujiin ...

Kenttään val sijoitettavan arvon laiska evaluointi ilmaistaan määreellä lazy.

Pikku esimerkki interaktiivisella tulkilla:

scala> val boo = {println("evaluoidun"); "böö"}  // "ahkera" evaluoidaan saman tien
evaluoidun
boo: String = böö

scala> lazy val boo = {println("evaluoidun"); "böö"} // laiskaa ei vielä
boo: String = <lazy>

scala> boo  // vasta tämä saa aikaan funktion suorituksen
evaluoidun
res0: String = böö

scala> boo   // mutta se suoritetaan vain kerran!
res1: String = böö

Tarkastellaan sitten tilannetta, jossa jotain "isoa" kokoelmaa manipuloidaan useammalla tavalla:

val lista = List(3, -6, 23, 92, 4, 66)  // tämä on se "iso" kokoelma

lazy val eiNegat = lista.filter(_>=0)
lazy val tuplaa  = eiNegat.map(_*2)
lazy val plus1   = tuplaa.map(_+1)

println(plus1)  // List(7, 47, 185, 9, 133)
Nyt muuttujille (!) eiNegat, tuplaa ja plus1 ei lasketa alkuarvoa määrittelyjen yhteydessä, vaan vasta tulostuskomento käynnistää funktio-olion plus1, joka käynnistää funktio-olion tuplaa, joka käynnistää funktio-olion eiNegat,
(joka suuteli tyttöä, joka huuteli lehmää, joka puski koiraa, joka puri kissaa, joka söi sen hiiren, joka möi sen mallaspussin, joka oli talossa, jonka rakensi Jussi!)

Seuraus on, että koko laskenta tehdään yhdellä kerralla ja vain jos joku tulee kutsuneeksi funktio-olioa plus1. (Laitoksella kehitelty hyvin hajautettu Spark-järjestelmä käyttää tämän tyyppistä tekniikkaa!)

Tutkaillaanpa sitten vielä tilannetta, jossa olion kentät ovat laiskoja tai "tavallisia" valleja:

class C(lista: List[Int]) {
  lazy val eiNegat = lista.filter(_>=0)
  lazy val tuplaa  = eiNegat.map(_*2)
  lazy val plus1   = tuplaa.map(_+1)
}
val a = new C(List(1,2,-3,4))
println(a.plus1)                    // List(3, 5, 9)
val b = new C(List(10,20,-30,40))
println(b.plus1)                    // List(21, 41, 81)

class CC(lista: List[Int]) {
  val eiNegat = lista.filter(_>=0)
  val tuplaa  = eiNegat.map(_*2)
  val plus1   = tuplaa.map(_+1)
}
val aa = new CC(List(1,2,-3,4))
println(aa.plus1)                   // List(3, 5, 9)
val bb = new CC(List(10,20,-30,40)) 
println(bb.plus1)                   // List(21, 41, 81)
Aivan kuin mitään eroa ei olisi laiskan ja "ahkeran" välillä, vaan laitetaanpas vähän aputulostuksia, niin kuinkas sitten käykään ...
class C(lista: List[Int]) {
  lazy val eiNegat = {println("eiNegat"); lista.filter(_>=0)}
  lazy val tuplaa  = {println("tuplaa"); eiNegat.map(_*2)}
  lazy val plus1   = {println("plus1"); tuplaa.map(_+1)}
}

val a = new C(List(1,2,-3,4))
println("Ei vielä kutsuttu metodia plus1")
println(a.plus1)

println("----------------")

class CC(lista: List[Int]) {
  val eiNegat = {println("eiNegat"); lista.filter(_>=0)}
  val tuplaa  = {println("tuplaa"); eiNegat.map(_*2)}
  val plus1   = {println("plus1"); tuplaa.map(_+1)}
}

val aa = new CC(List(1,2,-3,4))
println("Ei vielä kutsuttu metodia plus1")
println(aa.plus1)
Tulostus on
Ei vielä kutsuttu metodia plus1
plus1
tuplaa
eiNegat
List(3, 5, 9)
----------------
eiNegat
tuplaa
plus1
Ei vielä kutsuttu metodia plus1
List(3, 5, 9)

Olion kenttien arvojen laiska evaluointi voi liittyä esimerkiksi tilanteeseen, jossa joitain raskaita tai tilaa vieviä laskentoja tai tilanvarauksia tehdään vain tarvittaessa.