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

3 Pikku jatkeita

Muutettu viimeksi 23.3.2010 / 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 taulukko-olion luonti oli mahdollista. 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öä, ks. vaikkapa Ohjelmoinnin jatkokurssin kohtaa 5.1 Poikkeuksista: Virheiden käsittelyä. 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", "seuralaisolio". Näihin palataan.

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

Listoista

Muuttujat ja muutettavat oliot liittyvät perinteiseen tilalliseen ohjelmointiin. Oikea funktionaalinen ohjelmointi on tilatonta: mikään ei muutu, arvoista vain lasketaan uusia arvoja, joista lasketaan uusia arvoja,... Scalan taulukot liittyvät siis ensinmainittuun tyyliin.

Listat sen sijaan 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 roskienkerääjät alkavat olla aika 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.")

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

  val twoThree = List(2, 3)
  val oneTwoThree = 1 :: twoThree
  println(oneTwoThree)
Tätä operaatiota kutsutaan perinteisesti nimellä "cons", koska se konstruoi uuden listan alkiosta ja 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:
  val oneTwoThree =  Nil.::(3).::(2).::(1)
  println(oneTwoThree)

Listoille on leegioittain operaatioita, mutta alkion liitämistä listan loppuun ei ole. Perustelut liittyvät tehokkuuteen...

Operaatioita ja ohjelmointitapoja [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.sort((s, t) =>
  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ä. Monikko on siis myös 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)
  println(pair._2)

Joukot ja assosiaatiolistat

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

Tekniikka on toteutettu Scalan piirteiden ("trait") peritymishierarkian avulla. Näistä myöhemmin.

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

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

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

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

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"))

  val kopioko = jetSet
  jetSet += "Myrsky"
  println(kopioko)    // Set(Airbus, Lear, Boeing, Myrsky)

  println(jetSet + "Caravelle") // Set(Airbus, Lear, Caravelle, Boeing, Myrsky)
Huom: Molemmat Set-versiot itse asiassa ovat piirteitä (trait), jollaisista ei voi luoda ilmentymiä. Niiden factory-metodit todellisuudessa luovat ilmentymiä luokasta HashSet (esim.). Jos tällaisessa tilanteessa haluaa itse valita todellisen toteutusluokan, se käy samaan tapaan:
  import scala.collection.immutable.HashSet

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

Myös assosiaatiolistasta on joukkoa 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))
Ja immutaabeli ilman importia:
  val romanNumeral = Map(
    1 -> "I", 2 -> "II", 3 -> "III", 4 -> "IV", 5 -> "V"
  )
  println(romanNumeral(4))

Tiedoston 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)
      print(line.length +" "+ line)
  else
    Console.err.println("Please enter filename")
Ilmaus Source.fromFile(args(0)) avaa (jos voi) parametrina 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
      print(padding + line.length +" | "+ line)
    }
  }
  else
    Console.err.println("Please enter filename")