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

1 Johdantoa

Muutettu viimeksi 22.3.2010 / Sivu luotu 3.3.2010 / [oppikirjan esimerkit] / [Scala]

Ohjelmointikieliä on paljon, hyvin paljon. Scala on yksi uusi monien joukossa. Kielen viralliset sivut ovat osoitteessa http://www.scala-lang.org/ . Tuolta löytyy tietoa laidasta laitaan spesifikaatiosta ja APIsta aloittelijan oppaisiin.

Tämän sivun sisältöä:

Taustaa ja ominaisuuksia

Scalalla on monia kiinnostavia ominaisuuksia. Seuraavassa on pieni ja epätäydellinen luettelo joistakin kielen ominaisuuksista. Luettelossa on muutamia sivun sisäisiä linkkejä kyseisen ominaisuuden esittelyyn.

Pikku ohjelman laatiminen on vaivatonta

Scalassa kirjoitusvaivaa ja erilaisia aloittelijalle käsittämättömiä "loitsuja" on paljon vähemmän kuin Javassa. Kääntäjä ei vaadi puolipistettä, jos se muutenkin osaa päätellä ilmaisujen rajat. Jos muuttujan tyyppi selviää käyttöyhteydestä, tyyppiä ei tarvitse mainita, jne., jne.

Esimerkki 1: Ohjelma laskee syöttölukujen keskiarvon. Lukumäärä pyydetään.

Esimerkki 2: Myös olioiden kanssa askartelu on Javaa paljon vaivattomampaa. Seuraavassa peruskurssin "ensimmäinen olio"

Scala-ohjelman suorittamisen tapoja

  1. Scalaa voidaan suoraan sellaisenaan käyttää "laskimena", antaa ohjelmarivejä suoraan tulkille (komento scala) (käyttäjä kirjoittaa kehoitteen perään - mahdollisesti useita rivejä, jos rakenne on monirivinen - Scala vastaa ilman rivin aloitinmerkkiä; nuo "res n":t ovat arvoille generoituja tunnuksia). Tässä yksi istunto:
    scala> 1+2
    res0: Int = 3
    
    scala> println("Hoi maailma!")
    Hoi maailma!
    
    scala> var a=5
    a: Int = 5
    
    scala> while (a>0) {
         | println(a)
         | a=a-1
         | }
    5
    4
    3
    2
    1
    
    scala> def neliösumma(a:int, b:Int)=
         | a*a+b*b
    neliösumma: (int,Int)Int
    
    scala> neliösumma(3,4)
    res3: Int = 25
    
    scala> exit
    
  2. Kielen ilmauksia voidaan sellaisinaan kirjoittaa teksitiedostoksi, joka suoritetaan ilmauksella scala tiedostonimi. Tällä tavoin käytettynä Scalaa voitaisiin kutsua "skriptikieleksi". Esimerkkinä tiedosto tervehdi.ohjelma:
    println("Montako tervehdystä?")
    var lkm = readInt
    while (lkm>0) {
      println("Hoi maailma!")
      lkm = lkm-1
    }
    
    Suoritus komennolla
    scala tervehdi.ohjelma

  3. Scala-ohjelma voidaan kääntää Javan tavukoodiksi Java-virtuaalikoneella suoritettavaksi. Nyt tarvitaan pääohjelma, jotta virtuaalikone tietäisi, ketä kutsua. Scalassa ei ole "static"-kalustoa; sellaisen vastineena käytetään ns. singleton-olioita (eli "ainokaisia" Koskimiehen Kaitsun käännöksenä). Syntaksi on yksikertainen ja luonteva. Äskeinen ohjelma on muokattava muotoon
    object tervehdi {
      def main(args: Array[String]) = {
        println("Montako tervehdystä?")
        var lkm = readInt
        while (lkm>0) {
          println("Hoi maailma!")
          lkm = lkm-1
        }
      }
    }
    
    
    Ohjelma Javan tapaan käännetään ensin tavukoodiksi
    scalac tervehdi.scala
    ja myös suoritetaan Javan tapaan komennolla
    scala tervehdi

  4. Scalan "skripti" voidaan helposti muuttaa myös Linuxin tai Windowsin komentotulkille annettavaksi skriptiksi. Esimerkkinä edellä nähty esimerkki Linuxin skriptinä:
    #!/usr/bin/env scala
    !#
    
    println("Montako tervehdystä?")
    var lkm = readInt
    while (lkm>0) {
      println("Hoi maailma!")
      lkm = lkm-1
    }
    
    Ohjelma käynnistyy yksinkertaisesti tiedostonimellä. Suoritusoikeus tiedostolle on luonnollisesti annettava. Jos Linuxissa turvallisuussyistä "työhakemisto ei ole hakupolulla", käynnistyksessä tiedostonimen eteen pitää kirjoittaa "./" Koko istunnon ajaksi "työhakemiston voi lisätä hakupolkuun" komennolla "PATH="$PATH:.". Jos tuon komennon lisää ~/.bashrc:hen (ei-login-shellit) ja ~/.bash_profile:en (login-shellit) "työhakemisto on hakupolulla" oletusarvoisesti.

Quicksort kahdella tyylillä

  1. Pikajärjestäminen voidaan ohjelmoida tavalliseen algoritmiseen ("proseduraaliseen") tapaan:
    def sort(xs: Array[Int]) {
      def swap(i: Int, j: Int) {
        val t = xs(i); xs(i) = xs(j); xs(j) = t
      }
      def sort1(l: Int, r: Int) {
        val pivot = xs((l + r) / 2)
        var i = l; var j = r
        while (i <= j) {
          while (xs(i) < pivot) i += 1
          while (xs(j) > pivot) j -= 1
          if (i <= j) {
            swap(i, j)
            i += 1
            j -= 1
          }
        }
        if (l < j) sort1(l, j)
        if (j < r) sort1(i, r)
      }
      sort1(0, xs.length - 1)
    }
    
    
    Tässä huomiota kannattaa kiinnittää mm. siihen, miten Scala Algolin ja Pascalin tapaan sallii sisäkkäiset aliohjelmat meille vanhemmille tutuin näkyvyyssännöin ;-). C-pohjaisissa kielissä - Java mukaan lukien! - aliohjelmarakenne on "litteä": niissä aliohjelma ei voi sisältää omia "pikku apulaisiaan" Algolin ja Pascalin tapaan!
    Taulukon indeksoinnissa Scala käyttää kaarisulkeita tuttujen hakasulkeiden síjaan.

  2. Myös funktionaalinen tyyli Scalalla sujuu:
    def sort(xs: Array[Int]): Array[Int] =
      if (xs.length <= 1) xs
      else {
        val pivot = xs(xs.length / 2)
        Array.concat(
          sort(xs filter (pivot >)),
          xs filter (pivot ==),
          sort(xs filter (pivot <))
        )
    }
    
    Tässä huomio kannattaa kiinnittää muutamaan seikkaan: Array.concat liittää yhteen kolme osataulukkoa. ensimmäiseen suodatetaan kaikki jakoalkiota pienemmät alkiot, keskimmäiseen jakoalkion kanssa yhtä suuret ja kolmanteen suuremmat. Suodattimelle (filter) annetaan argumenttina ns. predikaattifunktio tyyliin "pivot >", mikä todellakin tarkoittaa sitä, että vertailun operaatio annetaan parametrina. Scalassa funktiot ovat arvoja siinä kuin numeeriset arvotkin, funktioita voidaan välittää parametreina, muuttujan arvona voi olla funktio, funktio voi palauttaa arvonaan funktion, käytettävissä ovat funktioliteraalit siinä kuin numeeriset literaalitkin...

Esimerkkiominaisuuksia

Kirjan luvusta 1: A Scalable Language

Oppikirjan esimerkejä luvusta 1 kommentoituna ja laajennettuna:

Asiat voivat olla helppoja:

  var capital = Map("US" -> "Washington", "France" -> "Paris")
  capital += ("Japan" -> "Tokyo")
  println(capital("France"))   // Paris

  var pk =  Map("Suomi" -> "Helsinki", "Ruotsi" -> "Tukholma")
  println(pk("Ruotsi"))  // Tukholma

  println(pk)  // Map(Suomi -> Helsinki, Ruotsi -> Tukholma)
  println(pk + ("Viro" -> "Tallinna"))  // Map(Suomi -> Helsinki, Ruotsi -> Tukholma, Viro -> Tallinna)
  println(pk - ("Ruotsi"))  // Map(Suomi -> Helsinki)
Helppoa ja hauskaa. Näyttää vaarattomalta, mutta sisältää itse asiassa mielenkiintoisia juttuja:

Omat tyypit ja operaatiot:

Scalassa BigInt on kirjastossa. Vastaa Javan BigInteger-luokkaa (itse asiassa "wräppää" tuon luokan. Kyseessä siis on kooltaan rajoittamaton kokonaislukutyyppi. Mutta ero löytyy helppokäyttöisyydessä.

Scalalla voi ohjelmoida seuraavasti:

  def factorial(x: BigInt): BigInt = 
    if (x == 0) 1 else x * factorial(x - 1)

  println(factorial(30))  // 265252859812191058636308480000000
Toki Javan luokkaakin voi käyttää, mutta, mutta:
  import java.math.BigInteger
  def factorial(x: BigInteger): BigInteger =
    if (x == BigInteger.ZERO)
      BigInteger.ONE
    else
      x.multiply(factorial(x.subtract(BigInteger.ONE)))

  println(factorial(new BigInteger("30")))  // 265252859812191058636308480000000

Luokka BigInt nyt vaan sattuu olemaan Scalan standardikirjastossa. Ja sille nyt vaan sattuu olevan ohjelmoitu mm. metodit +, -, *, ... Ja toki metodikutsun saa kirjoittaa esim. Javasta tuttuun tapaan:

  def factorial(x: BigInt): BigInt =
    if (x == 0) 1 else x.*(factorial(x - 1))
Myös molemmat:
  def factorial(x: BigInt): BigInt =
    if (x == 0) 1 else x.*(factorial(x.-(1)))
Ja toki yhtäsuuruusvertailun voi tehdä toisinkin
  def factorial(x: BigInt): BigInt =
    if (x.equals(0)) 1 else x.*(factorial(x.-(1)))
Vielä kerran: BigInt siis ei ole kieleen sisäänrakennettu tyyppi vaan kirjastoon ohjelmoitu tyyppi. Vastaavia voi siis tehdä myös itse!

Kontrollirakenteetkin voivat olla kirjastorutiineita:

Esimerkkinä Scalan kirjastosta ns. aktorimalli rinnakkaisohjelmointiin. Aktori osaa lähettää ja vastaanottaa viestejä muiden aktoreiden kanssa. Se ei jaa yhteistä muistia muiden aktoreiden kanssa. Viesti lähetetään !-operaatiolla:

  vastaanottaja ! viesti
Operaation jälkeen viestin lähettäjän toiminta jatkuu välittömästi, so. ei odotella kuittausta.

Jokaisella aktorilla on postilaatikko, johon aktorin saamat viestit tulevat jonottamaan. Postilaatikon viestit vastaanotetaan receive-operaatiolla:

  receive {
    case Msg1 => ... // handle Msg1
    case Msg2 => ... // handle Msg2
    // ...
  }
receive-lohko sisältää "message pattern" -ehtoja. Postilaatikosta valitaan vanhin sellainen viesti, johon täsmää jokin lohkon ehto, ja tuohon ehtoon liittyvä operaatio suoritetaan. Jos laatikko on tyhjä tai jos mikään laatikon viesteistä ei täsmää yhteenkään ehtoon, odotellaan...

Aktoreita kirjoitellaan tyyliin:

  actor { 
    var sum = 0
    loop {
      receive {
        case Data(bytes)       => sum += hash(bytes)
        case GetSum(requester) => requester ! sum
      }
    }
  }
Oleellista tässä on, ettei yksikään nimistä actor, loop, receive, tai operaatio "!" ole Scala-kieleen sisäänrakennettu. Ne kaikki ovat (taas kerran!) kirjastoon ohjelmoituja välineitä. (Esim. Erlang-kielessä - josta aktorit ovat kotoisin - ne ovat varattuja sanoja ja sisäänrakennettuja operaatioita.)

Ja siis taas kerran: Tällaisia siis voi tehdä myös itse.