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

17 Kokoelmista

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

Sivun sisältöä:

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.

Iteroitavat ja iteraattorit

Kokoelmien - niin muunneltavien (mutable) kuin kiinteidenkin (immutable) - yhteinen ylityyppi, "ylipiirreluokka", on Iterable. 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 tyyppiä on toteutettu piirreluokkina.

Iterable tarjoaa hyvin rikkaan rajapinnan, jonka saa toteuttamalla itse vain erittäin laihan rajapinnan (vrt. luku 12). Piirreluokassa on vain yksi abstrakti metodi,

    abstract def elements: 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 piirreluokka, 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 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. Sen tietenkin määrittelee ja ohjelmoi 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 numeroita.

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ä piirreluokan 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 elements = 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 elements = new ElainArpajaiset
}
val x = new K
var kissa, koira = 0
x.foreach( x => if (x=="kissa") kissa += 1 else koira += 1  )
println(kissa + " " + koira)

// Muutama esimerkkisuoritus:
// 5002334 4997666
// 5000385 4999615
// 5001516 4998484
// 5001768 4998232
// 4995602 5004398
Ei kai kissoja vain lievästi suosita? ;-)

Sekvenssejä

Joukot ja kuvaukset

Joukkojen operaatioita:
val nums = Set(1, 2, 3) 
	Creates an immutable set (nums.toString returns Set(1, 2, 3))
nums + 5 
	Adds an element (returns Set(1, 2, 3, 5))
nums - 3 
	Removes an element (returns Set(1, 2))
nums ++ List(5, 6) 
	Adds multiple elements (returns Set(1, 2, 3, 5, 6))
nums -- List(1, 2) 
	Removes multiple elements (returns Set(3))
nums ** Set(1, 3, 5, 7) 
	Takes the intersection of two sets (returns Set(1, 3))
nums.size 
	Returns the size of the set (returns 3)
nums.contains(3) 
	Checks for inclusion (returns true)
------------------------------------------------------------------
import scala.collection.mutable
	Makes the mutable collections easy to access
val words = mutable.Set.empty[String]  
	Creates an empty, mutable set (words.toString returns Set())
words += "the" 
	Adds an element (words.toString returns Set(the))
words -= "the" 
	Removes an element, if it exists (words.toString returns Set())
words ++= List("do", "re", "mi")  
	Adds multiple elements (words.toString returns Set(do, re, mi))
words --= List("do", "re") 
	Removes multiple elements (words.toString returns Set(mi))
words.clear 
	Removes all elements (words.toString returns Set())
Kuvauksien operaatioita:
val nums = Map("i" -> 1, "ii" -> 2) 
	Creates an immutable map (nums.toString returns Map(i -> 1, ii -> 2))
nums + ("vi" -> 6) 
	Adds an entry (returns Map(i -> 1, ii -> 2, vi -> 6))
nums - "ii" 
	Removes an entry (returns Map(i -> 1))
nums ++ List("iii" -> 3, "v" -> 5) 
	Adds multiple entries (returns Map(i -> 1, ii -> 2, iii -> 3, v -> 5))
nums -- List("i", "ii") 
	Removes multiple entries (returns Map())
nums.size 
	Returns the size of the map (returns 2)
nums.contains("ii") 
	Checks for inclusion (returns true)
nums("ii") 
	Retrieves the value at a specified key (returns 2)
nums.keys 
	Returns the keys (returns an Iterator over the strings "i" and "ii")
nums.keySet 
	Returns the keys as a set (returns Set(i, ii))
nums.values 
	Returns the values (returns an Iterator over the integers 1 and 2)
nums.isEmpty 
	Indicates whether the map is empty (returns false)
------------------------------------------------------------------
import scala.collection.mutable
	Makes the mutable collections easy to access
val words = mutable.Map.empty[String, Int] 
	Creates an empty, mutable map
words += ("one" -> 1) 
	Adds a map entry from "one" to 1 (words.toString returns Map(one -> 1))
words -= "one" 
	Removes a map entry, if it exists (words.toString returns Map())
words ++= List("one" -> 1, "two" -> 2, "three" -> 3) 
 	Adds multiple map entries (words.toString returns Map(one -> 1, two -> 2, three -> 3))
words --= List("one", "two") 
 	Removes multiple objects (words.toString returns Map(three -> 3))