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

3 Alkeita: tietokokoelmia ja tiedoston lukemista

Muutettu viimeksi 25.2.2016 / Sivu luotu 10.3.2010 / [oppikirjan esimerkit] / [Scala]

Sivun sisältöä:

Taulukoista

Taulukko-olioita voidaan luoda ja käyttää seuraavaan tapaan:
  val greetStrings = new Array[String](3)
  
  greetStrings(0) = "Hello"
  greetStrings(1) = ", "
  greetStrings(2) = "world!\n"

  for (i <- 0 to 2)
    print(greetStrings(i))
Kiinnostavaa: Tyyppipäättelyn ansiosta yllä nähty kirjoitustapa oli mahdollinen. Pidemmästikin saa kirjoittaa:
 val greetStrings: Array[String] = new Array[String](3)
Javan tapaan taulukon tyypin määrää alkioiden tyyppi, ei niiden lukumäärä, joten var greetStrings: Array[String]-muuttuja voisi saada arvokseen milloin minkin mittaisia String-taulukoita.

Kaarisulkeiden käyttämiselle taulukoiden indeksoinnissa on perusteensa:

Taulukon voi luoda myös luettelemalla alkiot:

  val numNames = Array("zero", "one", "two")
Tässäkin kaarisulkeet tarkoittavat apply-metodin kutsua:
  val numNames = Array.apply("zero", "one", "two")

Array-luokan apply-metodi on itse asiassa ns. factory-metodi, joka luo uuden Array-olion annetulla sisällöllä. Menettely vastaa Javan ja C++:n ns. staattisen luontimetodin käyttöä. Koska Scalassa mikään ei ole static, factory-metodi toteutetaan luokkaan liittyvän singleton-olion ("ainokaisen") metodina. Näin käytettyä "ainokaista" kutsutaan Scala-jargonissa nimellä companion object, kumppaniolio. Näihin palataan.

Huom: Scalan funktiolla voi olla myös vaihteleva määrä parametreja, ns. "toistuvia parametreja". Näihinkin palataan.

Listoista

Muuttujat ja muutettavat oliot liittyvät perinteiseen tilalliseen ohjelmointiin (tila=state). Puhtaasti funktionaalinen ohjelmointi on tilatonta: "muuttujien" arvot eivät muutu, niistä vain lasketaan uusia arvoja, joista lasketaan uusia arvoja,... Scalan taulukot liittyvät siis ensinmainittuun tyyliin.

Funktionaaliset ohjelmointikielet ovat oikestaan ns. "assign once" -kieliä. Muuttujat siis saavat arvon täsmälleen kerran. Ohjelman suorituksen edetessä muuttujien joukko kuitenkin yleensä kasvaa eli tavallaan ohjelman tila suorituksen edetessä kuitenkin muuttuu! Churchin Lambda-kalkyyli on oikeastaan ainoa "ihan puhdas" funktionaalinen kieli – siinä ei ole lainkaan muuttujia ohjelmointikielten mielessä, "laatikoita joihin sijoitetaan arvo". Nimettyjä funktioitakaan ei siis ole, on vain rakenne, jota esimerkiksi Scalassa kutsutaan funktioliteraaleiksi: argumenttien kuvauksia arvoille.

Listat Scalassa ovat muuttumattomia, "immutaabeleita". Java-ohjelmoijalle periaate on tuttu String-tyypistä: kerran luotua String-oliota ei voi kerta kaikkiaan muuttaa millään keinoin. Vaikka tarjolla on suuri joukko operaatioita String-arvojen "muuttamiseen", kaikki "muutetut" arvot ovat todellisuudessa uusia String-olioita. Ja kun viimeinenkin viite johonkin String-olioon poistuu, roskienkerääjä voi halutessaan vapauttaa tuon olion varaaman tilan muuhun käyttöön.

Scalassa on paljon kalustoa, jolla roskienkerääjää voidaan samaan tapaan ylensyöttää. Johdannossa jo tutustuttiin assosiaatiolistaan Map. Roskienkerääjän syöttäminen saattaa aluksi kuulostaa hullulta, mutta pidetäänpä mielessä vaikkapa se, että "immutaabelia" oliota ei tarvitse millään tavoin suojata rinnakkaisohjelmissa. Ja rinnakkaisuutta kohden nykyään mennään... Toisaalta myös roskienkerääjät alkavat olla melko hyviä nykyään.

Esimerkkejä:

  val oneTwoThree = List(1, 2, 3)
  val oneTwo = List(1, 2)
  val threeFour = List(3, 4)

  val oneTwoThreeFour = oneTwo ::: threeFour

  println(""+ oneTwo +" and "+ threeFour +" were not mutated.")
  println("Thus, "+ oneTwoThreeFour +" is a new list.")
Tulostus:
List(1, 2) and List(3, 4) were not mutated.
Thus, List(1, 2, 3, 4) is a new list.

Operaatio "::" liittää alkion listan alkuun:

  val twoThree = List(2, 3)
  val oneTwoThree = 1 :: twoThree
  println(oneTwoThree)  // List(1, 2, 3)
Tätä operaatiota kutsutaan perinteisesti – jo Lispistä alkaen! – nimellä "cons", koska se konstruoi uuden listan annetusta alkiosta ja annetusta listasta.

Tyhjä lista on Nil. Uusi lista voidaan luoda myös lisäämällä tyhjään listaan alkiot yksitellen:

  val oneTwoThree = 1 :: 2 :: 3 :: Nil
  println(oneTwoThree)
Tämä ehkä vähän selventää tuota "assosiatiivisuutta oikealta vasemmalle". Saman voi siis kirjoittaa myös pisteellisinä metodikutsuina:
  val oneTwoThree =  Nil.::(3).::(2).::(1)
  println(oneTwoThree)

Kielessä on myös operaatio ":+" alkion liittämiseksi listan loppuun:

val oneTwoThree = List(1, 2, 3)
println(oneTwoThree :+ 666)   // List(1, 2, 3, 666)
Tätä ei kuitenkaan suositella käytettäväksi, koska operaation työläys kasvaa lineaarisesti listan pituuden funktiona!

Listoille on leegioittain operaatioita:

Esimerkkejä operaatioista ja ohjelmointitavoista: [ks. myös API, ks. List]:

List() or Nil                       The empty List
List("Cool", "tools", "rule")       Creates a new List[String] with the three values
                                    "Cool", "tools", and "rule"
val thrill = "Will" :: "fill" ::
             "until" :: Nil         Creates a new List[String] with the three values
                                    "Will", "fill", and "until"
List("a", "b") ::: List("c", "d")   Concatenates two lists (returns a new List[String] with values 
                                    "a", "b", "c", and "d")
thrill(2)                           Returns the element at index 2 (zero based) of the thrill list 
                                    (returns "until")
thrill.count(s => s.length == 4)    Counts the number of string elements in thrill that have length 4 
                                    (returns 2)
thrill.drop(2)                      Returns the thrill list without its first 2 elements 
                                    (returns List("until"))
thrill.dropRight(2)                 Returns the thrill list without its rightmost 2 elements 
                                    (returns List("Will"))
thrill.exists(s => s == "until")    Determines whether a string element exists in thrill that has the 
                                    value "until" (returns true)
thrill.filter(s => s.length == 4)   Returns a list of all elements, in order, of the thrill list that 
                                    have length 4 (returns List("Will", "fill"))
thrill.forall(s => s.endsWith("l")) Indicates whether all elements in the thrill list end with the 
                                    letter "l" (returns true)
thrill.foreach(s => print(s))       Executes the print statement on each of the strings in the thrill 
                                    list (prints "Willfilluntil")
thrill.foreach(print)               Same as the previous, but more concise (also prints "Willfilluntil")
thrill.head                         Returns the first element in the thrill list (returns "Will")
thrill.init                         Returns a list of all but the last element in the thrill
                                    list (returns List("Will", "fill"))
thrill.isEmpty                      Indicates whether the thrill list is empty (returns false)
thrill.last                         Returns the last element in the thrill list (returns "until")
thrill.length                       Returns the number of elements in the thrill list (returns 3)
thrill.map(s => s + "y")            Returns a list resulting from adding a "y" to each string element
                                    in the thrill list (returns List("Willy", "filly", "untily"))
thrill.mkString(", ")               Makes a string with the elements of the list 
                                    (returns "Will, fill, until")
thrill.remove(s => s.length == 4)   Returns a list of all elements, in order, of the thrill list 
                                    except those that have length 4 (returns List("until"))
thrill.reverse                      Returns a list containing all elements of the thrill list in 
                                    reverse order (returns List("until", "fill", "Will"))
thrill.sortWith((s, t) =>    HUOM: sort on deprecated!
  s.charAt(0).toLowerCase < 
    t.charAt(0).toLowerCase)        Returns a list containing all elements of the thrill list in 
                                    alphabetical order of the first character lowercased 
                                    (returns List("fill", "until", "Will"))
thrill.tail                         Returns the thrill list minus its first element (returns
                                    List("fill", "until"))
...



Monikot eli "tuppelit" eli "ännäköt"

Monikko ("tuple") on kuin lista, mutta sen alkiot voivat olla keskenään eri tyyppisiä. Myös monikko on muuttumaton, "immutaabeli".

Keskenään eri tyyppisistä arvoista muodostuva järjestetty jono on vallan mainio esimerkiksi tilanteessa, jossa funktion halutaan palauttavan useampia arvoja! Ei tarvitse Javan tapaan määritellä jotakin apuluokkaa arvojen kokoelmaksi.

Tuppelin tyyppejä ovat esim. Tuple2[Int, String], Tuple5[Int, String, Boolean, Double, String], ... Eli tyyppi muodostuu n-tuppelin n:stä ja jonon alkioden tyypeistä.

  val pair = (99, "Luftballons")
  println(pair._1)  // 99
  println(pair._2)  // Luftballons

Joukot ja assosiaatiolistat

Listat ja monikot ovat aina muuttumattomia. Joukot (Set) ja assosiaatiolistat (Map) voivat olla muuttumattomia tai muutettavia.

Tekniikka on toteutettu Scalan piirretyyppien ("trait") periyttämisen avulla. Näistä lisää myöhemmin.

Oletusarvoisesti käytössä oleva Set-versio on "immutaabeli":

  var jetSet = Set("Boeing", "Airbus")
  jetSet += "Lear"
  println(jetSet.contains("Cessna"))  // false

  val kopioko = jetSet
  jetSet += "Myrsky"

  println(kopioko)    // Set(Boeing, Airbus, Lear) // EI MUUTOSTA
  println(jetSet + "Caravelle") // Set(Airbus, Myrsky, Lear, Caravelle, Boeing)

Jos halutaan käyttää muutettavaa versiota Set-luokasta, se on joko tuotava (import) käännösyksikköön tai kutsuttava täydellisellä nimellä, joka sisältää pakkauspolun (scala.collection.mutable.Set):

  import scala.collection.mutable.Set

  val jetSet = Set("Boeing", "Airbus")
  jetSet += "Lear"
  println(jetSet.contains("Cessna"))  // false

  val kopioko = jetSet
  jetSet += "Myrsky"

  println(kopioko)    // Set(Myrsky, Airbus, Boeing, Lear)
  println(jetSet + "Caravelle")  // Set(Myrsky, Airbus, Boeing, Lear, Caravelle)
  println(kopioko + "Caravelle") // Set(Myrsky, Airbus, Boeing, Lear, Caravelle)
Huom: Molemmat Set-versiot itse asiassa luodaan piirretyypin (trait) avulla. Piirretyypeistä ei kuitenkaan voi luoda ilmentymiä! Mutta piirretyypeilläkin voi olla oliokumppani, jossa on factory-metodi. Set-piirretyyppien factory-metodit itse asiassa luovat ilmentymiä luokasta HashSet. Jos tällaisessa tilanteessa haluaa itse valita todellisen toteutusluokan, se käy seuraavasti:
  import scala.collection.immutable.HashSet

  val hashSet = HashSet("Tomatoes", "Chilies")
  println(hashSet + "Coriander")   // Set(Chilies, Tomatoes, Coriander)

Esimakua piirretyypeistä ja luokista:

Myös assosiaatiolistasta on Set-tyyppiä vastaavalla tavalla toteutettu muutettava ja muuttumaton versio:

Mutaabeli:

  import scala.collection.mutable.Map

  val treasureMap = Map[Int, String]()
  treasureMap += (1 -> "Go to island.")
  treasureMap += (2 -> "Find big X on ground.")
  treasureMap += (3 -> "Dig.")
  println(treasureMap(2))         // Find big X on ground.
Ja immutaabeli saadaan ilman importia:
  val romanNumeral = Map(
    1 -> "I", 2 -> "II", 3 -> "III", 4 -> "IV", 5 -> "V"
  )
  println(romanNumeral(4))  // IV

Huom: Myös -> on metodi. Erimerkiksi 1 -> "I" on mahdollista kirjoittaa muodossa (1).->("I"). Tässä tapahtuu ns. implisiittinen tyyppimuunnos. Niistä myöhemmin.

Tekstitiedoston lukemisesta (oikeastaan Scalan voimasta)

Esimerkkiohjelma listaa komentoriviparametrina annetun tiedoston siten, että joka rivin alkuun kirjoitetaan rivin pituus:
  import scala.io.Source

  if (args.length > 0)
    for (line <- Source.fromFile(args(0)).getLines)
      println(line.length +" "+ line)
  else
    Console.err.println("Please enter filename")
Ilmaus Source.fromFile(args(0)) avaa (jos voi) komentoriviparametrina annetun tiedoston ja palauttaa arvonaan Source-olion. Tämän olion getLines palauttaa iteraattorin Iterator[String], joka antaa tiedoston rivit yksi kerrallaan Stringeinä for-lauseen käyttöön.

Hienompi versio, jossa rivinpituudet kirjoitetaan saman mittaiseen kenttään ja jossa käytetään hienoja(?) välineitä:

  import scala.io.Source

  def widthOfLength(s: String) = s.length.toString.length

  if (args.length > 0) {
    val lines = Source.fromFile(args(0)).getLines.toList  // Hyi! ;-)

    val longestLine = lines.reduceLeft(
      (a, b) => if (a.length > b.length) a else b 
    ) 

    val maxWidth = widthOfLength(longestLine)

    for (line <- lines) {
      val numSpaces = maxWidth - widthOfLength(line)
      val padding = " " * numSpaces
      println(padding + line.length +" | "+ line)
    }
  }
  else
    Console.err.println("Please enter filename")