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

17 Iteroitavat ja iteraattorit

Muutettu viimeksi 26.4.2016 / Sivu luotu 22.4.2010 / [oppikirjan esimerkit] / [Scala]

Tässä luvussa vain lyhyesti kurkistetaan tapaan, jolla Scalan kokoelmaluokat on organisoitu. Tarkoitus on tällä esimerkillä vain havainnollistaa Scalan tyypillistä käyttöä ohjelma-arkkitehtuurissa. Parin keskeisen kokoelman työkalujakin silti luetellaan.

Kokoelmille - niin muunneltaville (mutable) kuin kiinteillekin (immutable) - on ylityyppi, "ylipiirretyyppi" eli trait Iterable. Muunneltaville ja kiinteille on oma versio pakkauksissa scala.collection.immutable ja scala.collection.mutable.

Kokoelmia on kolmea päätyyppiä: Järjestetyt jonot eli sekvenssit (Seq), joukot (Set) ja kuvaukset (Map):

Kokoelmien luokkahierarkia

Kuten "kuvasta näkyy", myös nuo kaikki kolme päätyyppiä on toteutettu piirretyyppeinä.

Iterable tarjoaa toteutettuna hyvin rikkaan rajapinnan, jonka saa käyttöön toteuttamalla itse vain erittäin laihan rajapinnan (vrt. luku 12). Piirretyypissä Iterable on vain yksi abstrakti metodi,

   def iterator: Iterator[A]
Kun siis kokoelmaluokalle toteuttaa iteraattorin, menetelmän käydä läpi kokoelman alkiot, saa valmiiksi ohjelmoituna kymmeniä metodeita (ks. API). Esimerkiksi seuraavat löytyvät juuri täältä: exists, filter, forall, foreach, indexOf, map, reduceLeft, toList, ... Eivätkä kaikki "ilmaiseksi" saadut metodit ole lainkaan mitään triviaaleja. Ja kuten jo noista esimerkkimetodeista näkyy, arkkitehtuuri perustuu hyvin syvästi funktioiden välittämiseen parametreina.

Alkaako jo valjeta eräiden Scalan ideoiden nerokkuus?

No mikäs sitten oli tuo ainoa toteutetavaksi pyydetty Iterator? Myös se on piirretyyppi, mutta Scala-kirjaston luokkahierarkiassa aivan eri paikassa kuin Iterable:

Iteraattorin luokkahierarkia

Iterablen tavoin Iterator sisältää suuren joukon toteutettuja metodeita - osin saman nimisiäkin - jotka saa käyttöönsä toteuttamalla kaksi perittyä abstraktia metodia:

    abstract def hasNext: Boolean
    abstract def next: A
Iteraattori ("kulkuri") on kuin sormi, joka osoittaa aluksi kokoelman "ensimmäiseen" alkioon ja osaa liikkua vain eteenpäin kokoelman alkioiden joukossa – mitä "eteenpäin" sitten tarkoittaakin missäkin rakenteessa. Tämän asian tietenkin määrää se, joka nuo metodit toteuttaa.

Tavallisesti kokoelmalla on "viimeinen" alkio, mutta tämä ei ole välttämätöntä iteroinnille; Iterator-tyyppinen iteraattori voisi iteroida vaikkapa pitkin piin likiarvon desimaaleja, joita riittää ihan niin paljon kuin halutaan...

Oppikirjan kaunis luonnehdinta Iterablen Iteratorin erosta (en osannut itse parempia sanoja tähän keksiä):

The difference between Iterable and Iterator is that trait Iterable represents types that can be iterated over (i.e., collection types), whereas trait Iterator is the mechanism used to perform an iteration. Although an Iterable can be iterated over multiple times, an Iterator can be used just once. Once you've iterated through a collection with an Iterator, you can't reuse it. If you need to iterate through the same collection again, you'll need to call elements on that collection to obtain a new Iterator.

Tehdään sormiharjoituksena yksi äärettömästi iteroitava luokka:

class ElainArpajaiset extends Iterator[String] {
    def hasNext = true
    def next = if (math.random < 0.5) "kissa" else "koira"
}

val x = new ElainArpajaiset
for (i <- 1 to 10)  print(x.next +", ")
println

// Esimerkkitulostus:
// kissa, kissa, kissa, koira, koira, koira, koira, koira, kissa, koira,

Ja toisena harjoituksena äärellinen iteraatio ja sen käyttö:
class ElainArpajaiset extends Iterator[String] {
    var i = 10
    def hasNext = i > 0
    def next = {i -= 1; if (math.random < 0.5) "kissa" else "koira"}
}

val x = new ElainArpajaiset
while (x.hasNext)  print(x.next +", ")
println

// Esimerkkitulostus:
// koira, kissa, koira, kissa, koira, kissa, koira, koira, kissa, kissa,
Kokeillaan vielä piirretyypin Iterable käyttöä:
class ElainArpajaiset extends Iterator[String] {
    var i = 10
    def hasNext = i > 0
    def next = {i -= 1; if (math.random < 0.5) "kissa" else "koira"}
}

class K extends Iterable[String] {
  def iterator = new ElainArpajaiset

}
val x = new K
x.foreach( x => print(x + ", ") )
println

// Esimerkkitulostus:
// koira, kissa, koira, koira, koira, kissa, koira, koira, koira, kissa,
Vaikka esimerkit olivat "pelkkää leikkiä" - ja vaikka Scalalla on kiva leikkiä(!) - selvästi näkee, miten vaivattomasti ja luontevasti Scalalla saa ohjelmoitua voimakkaita rakenteita!

Kokeillaanpa vielä, miten reilu tuo math.random-arvonta on:

class ElainArpajaiset extends Iterator[String] {
    var i = 10000000
    def hasNext = i > 0
    def next = {i -= 1; if (math.random < 0.5) "kissa" else "koira"}
}

class K extends Iterable[String] {
  def iterator = new ElainArpajaiset
}
val x = new K

println("kissoja koiria")

var kissa, koira = 0
x.foreach( x => if (x=="kissa") kissa += 1 else koira += 1  )
println(kissa + " " + koira)
Muutama esimerkkisuoritus:
kissoja koiria
4999379 5000621
4999595 5000405
4999368 5000632
4998156 5001844
4999904 5000096
Ei kai koiria vain lievästi suosita? ;-)