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

8 Funktiot ja sulkeumat

Muutettu viimeksi 1.4.2010 / Sivu luotu 29.3.2010 / [oppikirjan esimerkit] / [Scala]

Sivun sisältöä:

Metodi ja funktio

Scalassa kaikkia aliohjelmia on tapana nimittää funktioiksi vaikka ne eivät välttämättä aina olekaan funktioita matematiikan mielessä. Kun funktiota käytetään olio-ohjelmoinnin "aksessorina" tai jos funktio jossakin muussa mielessä(?) on olion tai luokan "jäsen", sitä kutsutaan perinteisesti myös metodiksi, "menetelmäksi" käsitellä jotakin oliota tai luokkaa.

Esimerkki klassisesta ohjelmointityylistä: [Vaikka oppikirja antaa ymmärtää, että tässä olisi jotenkin kyseessä olio-ohjelmointi ja funktiot siksi olisivat "metodeita", mielestäni tässä vain esitellään, miten Scalalla ohjelmoidaan ihan perinteiseen tyyliin "aliohjelmia".]

Ensin "kirjasto", jossa on julkisia työkalufunktioita ja niiden yksityisiä apufunktioita:

  import scala.io.Source

  object LongLines {

    def processFile(filename: String, width: Int) {
      val source = Source.fromFile(filename)
      for (line <- source.getLines) 
        processLine(filename, width, line)
    }
    private def processLine(filename: String, width: Int, line: String) {
      if (line.length > width)
        println(filename +": "+ line)
    }
  }
Ja sitten sovellus, joka käyttää kirjastoa:
  object FindLongLines {
    def main(args: Array[String]) {
      val width = args(0).toInt
      for (arg <- args.drop(1))
        LongLines.processFile(arg, width)
    } 
  } 
Käyttöesimerkki:
$ scala FindLongLines 73 RautatieUTF8.txt 
RautatieUTF8.txt: tullut. Vaan mitenkä? ... sitä Matti ei osannut itselleen selittää ... ja
RautatieUTF8.txt: kotoapäinhän sitä tullaan ... hyvää iltaa, ryökkynä ... terve, terve! ...
RautatieUTF8.txt: --Monienkin pyörien päällä kulki ... ja kun me seistiin siinä sen huoneen
RautatieUTF8.txt: --Vähät minä siitä, jos en näekään ... ja niinpä tuo jalkojakin pakottaa,
RautatieUTF8.txt: oikeaan käteen ja tämä tie viepi suoraan kirkolle ... on siinä punaiseksi
RautatieUTF8.txt: --Herra i--ihme! ... katso! kun on ... sehän on koreampi kuin pappilan...
RautatieUTF8.txt: --Elä he--ele--!... Siitäkö se nyt!... Hyvä isä siunatkoon ... siinäkö se
RautatieUTF8.txt: 1.C.  The Project Gutenberg Literary Archive Foundation ("the Foundation"
RautatieUTF8.txt: located in the United States, we do not claim a right to prevent you from
RautatieUTF8.txt: freely sharing Project Gutenberg-tm works in compliance with the terms of
RautatieUTF8.txt: posted on the official Project Gutenberg-tm web site (www.gutenberg.net),

Paikalliset funktiot

Edellisessä esimerkissä kirjastofunktion apufunktio toteutettiin yksityisenä funktiona. Scalan lohkorakennetta käyttäen apulaisen piilottaminen voidaan hoitaa myös sijoittamalla se tuon julkisen funktion sisään:
  import scala.io.Source

  object LongLines {

    def processFile(filename: String, width: Int) {

      def processLine(line: String) { // Huom: Lohkojen näkyvyyssääntöjen
        if (line.length > width)      //       takia tunnukset filename ja width
          print(filename +": "+ line) //       näkyvät tänne eikä niitä tarvitse
      }                               //       välittää parametreina.

      val source = Source.fromFile(filename)
      for (line <- source.getLines)
        processLine(line)
    }
  }

Funktiot first-class-arvoina

Funktioliteraali on ohjelmatekstiin kirjoitettu nimetön funktio (vrt. kokonaislukuliteraali, merkkijonoliteraali, ...).

Esimerkiksi

   (x: Int) => x + 1
on kokonaislukuparametrinsa seuraajan palauttava nimetön funktio.

Ohjelmatekstiin kirjoitettua literaalia vastaa suoritusaikana jokin arvo. Esimerkiksi desimaalilukuliteraalia "3.14" vastaa liukuluvuksi tulkittava bittien jono, jne.

Ja siis funktioliteraalia vastaa ohjelman suoritusaikana funktioarvo, jollaisia voi sijoitella muuttujiin, antaa parametreina, saada funktioden paluuarvoina, jne... Tällä tavoin käytettävät arvot ovat ns. first-class-arvoja.

Useimmissa "tavallisissa" ohjelmointikielissä tietenkin kokonais- ja liukuluvut ovat tällaisia. Javassa myös taulukot ovat tällaisia, mutta useimmissa kielissä taulukot ovat "second-class"-arvoja, koska ne eivät voi olla funktion paluuarvona, jne.

Scalassa (ja muissa funktionaalisissa kielissä) siis "perinnekielistä" poiketen myös funktiot ovat first-class-arvoja. Tämä merkitsee erityisesti siis sitä, että funktio voidaan välittää parametrina ja että funktio voi palauttaa arvonaan funktion!

Funktioarvot on toteutettu oliona, joten niitä voi käyttää kuin muitakin olioarvoja:

var increase = (x: Int) => x + 1
println(increase(10))  // 11

increase = (x: Int) => x + 100 // muuttujaa voi tietenkin muuttaa!
println(increase(10))  // 110

// jne. parametrivälitys, yms.
Funktioarvot ovat oliota, joihin kääntäjä liittää piirteitä, "mixaa traitteja", scalamaiseen tapaan: Every function value is an instance of some class that extends one of several FunctionN traits in package scala, such as Function0 for functions with no parameters, Function1 for functions with one parameter, and so on. Each FunctionN trait has an apply method used to invoke the function.

Huom: Koska funktioarvo ja funktion arvo voivat helposti sekoittua keskenään, saattaa olla turvallisempaa kutsua funktioarvoja nimellä funktio-olio!

Funktiot siis ovat funktioita matematiikkaa lavaeammassa merkityksessä; sivuvaikutukset yms. ovat mahdollisia:

var apua = 0

val increase = (x: Int) => {
  apua = 13                 // ... sivuvaikutuksia ...
  println("Kuinkas")
  println("nyt")
  println("käy?")
  x + 666
}
println(increase(10))  // Kuinkas
                       // nyt
                       // käy?
                       // 676

println(apua)          // 13

Monet Scalan valmiit kirjastorutiinit on toteutettu siten, että parametrina annetaan funktioarvo - tyypillisesti muttei välttämättä funktioliteraalina. Esim. kokoelmaluokille määritelty foreach on tällainen. Ja esimerkiksi filter-metodilla voi suodattaa alkioita kokoelmaluokkien ilmentymistä totuusarvoisella funktioparametrilla, väittämällä, jonka totuusarvo määrää mukaan otettavat alkiot:
val someNumbers = List(-11, -10, -5, 0, 5, 10)
someNumbers.foreach((x: Int) => println(x))

println( someNumbers.filter((x: Int) => x > 0) )  // List(5, 10)
Scalan päättelymekanismien ansiosta kirjoitusvaivoja voi usein vähentää:
  someNumbers.filter((x) => x > 0)
  // tai myös:
  someNumbers.filter(x => x > 0)


Paikanpitäjäparametrit

Paikanpitäjillä "_" (placeholder) voidaan jättää antamatta edes nimiä funktioliteraalien muodollisille parametreille:
val f = (_: Int) * (_: Int)
println(f(7, 8))                         // 56
              
val g = (_: String) + " ja " + (_: String)
println( g("kissa", "koira") )           // kissa ja koira  
Näin voi menetellä, jos funktion rungossa viitataan kuhunkin muodolliseen parametriin tasan kerran ja parametrien kirjoitusjärjestyksessä. Tyyppipäättelyn ansiosta tyyppininimiä voi toisinaan jättää kirjoittamatta.

Myös valmiin kaluston käyttö saadaan entistä näpsäkämmäksi(?):

val someNumbers = List(-11, -10, -5, 0, 5, 10)
println( someNumbers.filter(_ > 0) )  // List(5, 10)
Funktioliteraali "_ > 0" sanoo siis saman kuin "x => x > 0".

Huom: Jokainen funktioliteraalissa esiintyvä alaviiva siis tarkoittaa eri parametria. Jos samaan parametriin halutaan viitata useampaan kertaan, tämä tapa ei toimi.

Osittain sovelletut funktiot

Alaviiva "_" voi esittää myös kokonaista muodollisten parametrien listaa:

Esimerkiksi seuraavat ilmaukset tarkoittavat samaa:

  someNumbers.foreach(x => println(x)) // funktioliteraali tavalliseen tapaan
  someNumbers.foreach(println(_))      // funktioliteraali placeholder-parametrilla
  someNumbers.foreach(println _)       // placeholder koko parametrilistan sijaan

Ilmaus println _ on esimerkki osittain sovelletusta funktiosta, "partially applied function. (Tässä esimerkissä "osittain" tarkoittaa "ei lainkaan"...)"

Funktion kutsumista kutsutaan funktion soveltamiseksi (apply) argumentteihin eli todellisiin parametreihin. Esim. tässä

def sum(a: Int, b: Int, c: Int) = a + b + c
println(sum(1,2,3))
funktiota sum sovelletaan argumentteihin 1, 2 ja 3.

Osittain sovellettu funktio on sellainen, jolle ei annetakaan kaikkia argumentteja; ehkä ei ensimmäistäkään kuten tapauksessa println _.

Vastaava argumentiton esimerkki sum-funktion käytöstä:

def sum(a: Int, b: Int, c: Int) = a + b + c
val a = sum _
println(a(1, 2, 3))  // 6
Muuttuja a viittaa funktioarvoon (eli funktio-olioon), joka on saanut Function3-traitin mukana apply-aksessorin. Ja itse asiassa ilmaus a(1, 2, 3) tarkoittaa konkreettisesti a.apply(1, 2, 3). Ja näin siis saa myös itse kirjoittaa.

Huom. Vaikka luokan metodeita tai jonkin funktion sisältämiä paikallisia funktioita ei omalla nimellään saa esim. välitettyä parametrina, funktioarvon sijoittaminen val- tai var-muuttujaan tekee tämän mahdolliseksi! ("wrappäys", "käärepaperointi")

Edellä a:n arvoksi asetettu sum-funktio oli "osittain" sovellettu siten, ettei sitä oltu sovellettu ensimmäiseenkään parametriin... Mutta myös aidosti osittainen soveltaminen on mahdollista. Kiinnitetään sum-funktion ensimmäinen ja kolmas parametri:

def sum(a: Int, b: Int, c: Int) = a + b + c
val b = sum(1, _: Int, 3)
println(b(2))                // 6
println(b(5))                // 9
Saatiin aikaan yksiparametrinen funktio b:n arvoksi. Asetetaan siis b:n arvoksi yksiparametrinen funktioarvo, joka on tyypiltään
   (Int) => Int = <function>
Ja siis sen saama Function1-piirre on tuonut mukanaan yksiparametrisen apply-aksessorin!

Kun vaikkapa foreach-operaatiolle halutaan antaa parametrina sellainen "osittain sovellettu" funktio, jota ei ole lainkaan sovellettu, esim. println _ tai sum _, tuon alaviivankin saa jättää pois, koska kääntäjä tietää, että foreach sallii ainoastaan funktion parametrina:

someNumbers.foreach(println _)
 // voidaan kirjoittaa vieläkin lyhyemmin:   ... ja taas kääntäjä päättelee...
someNumbers.foreach(println)
[En ole lainkaan vakuuttunut, että tämä lyhennysmerkintä oli viisasta ottaa kieleen!]

Sulkeuma

Sulkeuman perusidea on antaa todellisena parametrina jokin koodinpätkä muualla suoritettavaksi siten, että tuossa vieraassa ympäristössä käynnistettynäkin koodin suoritus (eli viittaus vastaavaan muodolliseen parametriin) johtaa todellisen parametrin suorittamiseen todellisen parametrin omassa viittausympäristössä.

Ja tarkkaan ottaen sulkeuma on siis suoritusaikainen otus! Sellainen voidaan ohjelmatekstissä määrätä syntymään (so. ohjelmoida) kirjoittamalla funktioliteraali.

Vaikka esimerkiksi funktioliteraalia (x: Int) => x > 0 voidaan kutsua sulkeumaksi, se ei kuitenkaan vielä oikeastaan "sulje" mitään; sulkemisessa on kyse ns. vapaiden muuttujien kiinnittämisestä. Tuossa ilmauksessa x on ns. sidottu muuttuja, jonka merkitys on selvä: x on parametri, jolle annetaan aina arvo, kun ilmaus lasketaan.

Myös funktioliteraalin paikalliset tunnukset ovat sidottuja - niillä on aina sama merkitys:

val f = (x: Int) => {val kasvu = 2; x + kasvu}
println(f(6))  // 8

Mutta kun funktioliteraaliin kijoitetaan tunnus, joka ei tavalla tai toisella saa merkitystä literaali-ilmauksessa, kyseessä on ns. vapaa muuttuja. Tällaisen tunnuksen on luonnollisesti oltava määritelty ja näkyvissä siinä ohjelmakohdassa, johon literaali kirjoitetaan!

Huom: Sana "muuttuja" ilmauksissa "sidottu ja vapaa muuttuja" tarkoittaa muuttujaa matematiikan ja logiikan mielessä! Myös muut tunnukset kuin ohjelman muuttujien tunnukset (eli nimet) voivat olla sidottuja ja vapaita muuttujia - esim. vakioiden ja funktioiden tunnukset (eli nimet).

Kun kirjoitetaan (x: Int) => x + kasvu, funktioliteraalin ilmaus sisältää nyt muuttujan (matematiikan mielessä), joka ei saa merkitystä itse ilmauksesta. Ja tässä viimein on vapaa muuttuja! Ja nyt vastaa aletaan rakennella aitoja sulkeumia kiinnittämällä vapaat muuttujat (matematiikan mielessä), "sulkemalla" ne laskentarutiinin sisään!

Funktioliteraalia, jossa ei ole vapaita muuttujia, kutsutaan suljetuksi termiksi (closed term). Jos literaalissa on vapaita muuttujia, se on avoin termi (open term). Katsotaan, mitä tulkki sanoo tällaisista:

scala> (x: Int) => {val kasvu = 2; x + kasvu}
res3: (Int) => Int = <function>

scala> (x: Int) => x + kasvu
<console>:5: error: not found: value kasvu
       (x: Int) => x + kasvu
                       ^
Avoin termi ei siis kelpaa kääntäjälle, ellei vapaita muuttujia sidota. Sidotaan siis:
scala> var kasvu = 2
kasvu: Int = 2

scala>  (x: Int) => x + kasvu
res0: (Int) => Int = <function>
Jo alkoi kelvata! Ja nyt kun muuttujalla kasvu on merkitys, tulkki osaa luoda funktio-olion tuosta funktioliteraalista. Ja juuri nyt "suljetaan" jotakin, nimittäin muuttuja kasvu (ohjelmointikielen mielessä) liitetään muuttujaan kasvu (matematiikan mielessä). Sulkeuma on tuo funktio-olio, jonne on suljettu tuo (ohjelmoinnin) muuttuja kasvu.

Esimerkin sulkeumassa funktio-olioon suljettiin nimenomaan tuo (ohjelmoinnin) muuttuja, ei vain muuttujan arvoa Javan arvoparametrivälityksen tapaan. Katsotaanpas:

scala> var kasvu = 2       // HUOM: muuttuja
kasvu: Int = 2

scala> val f = (x: Int) => x + kasvu
f: (Int) => Int = <function>

scala> f(6)
res0: Int = 8

scala> kasvu = 7000        // HUOM: aito muutos muuttujaan 
kasvu: Int = 7000

scala> f(6)
res2: Int = 7006
Kuten nähdään, kerran synnytetty funktio-olio todellakin käyttää muuttujaa kasvu, ei tuon muuttujan arvoa olion syntyhetkellä.

Todellinen esimerkki sulkeumaan suljetun muuttujan muuttamisen mahdollisuuden järkevyydestä: Annetaan listan alkiot läpikäyvälle foreach-aksessorille aito sulkeuma:

val lukuja = List(3, 9, 12)
var summa = 0
lukuja.foreach(summa += _)  // Jokaista listan alkiota kohden käydään
                            // kasvattamassa kutsuympäristön muuttujaa!
println(summa)              // 24

Scala on kovin joustava (liian?) ilmauksissaan: Esimerkkejä erilaisista vaihtoehtoisista tavoista antaa tämä sama sulkeuma foreach-metodin suoritettavaksi:

val lukuja = List(3, 9, 12)
var summa = 0

lukuja.foreach(summa += _)
lukuja.foreach((x) => summa += x)
lukuja.foreach({(x) => summa += x})
lukuja.foreach({(x:Int) => summa += x})


Kaksi seuraavaa esimerkkiä on oikeastaan syventävien opintojen Ohjelmointikielten periaatteet -kurssin kamaa!

Parametrina annettavan sulkeuman kaikki funktio-olioon suljetut tunnukset - myös arvon saavat muuttujat - lasketaan ja ymmärretään kutsukohdan viiteympäristössä:

var a=100; var b=200

def teeJotakin(par: =>Unit){
  var a=1; var b=2
  par
  println(a+"/"+b)
}
def jokinRutiini {
  var a=10; var b=20
  teeJotakin(a=b)
  println(a+"/"+b)
}

jokinRutiini

teeJotakin(a=b)
println(a+"/"+b)

/* Tulostus:
   1/2
   20/20
   1/2
   200/200
*/
Toisena "syventävien" esimerkkinä omatekoinen toistorakenne:
def toista(algoritmi: =>Unit, kertaa:Int) {
  var x = 0; var y = 0;             // paikallisia, sattumalta samannimisiä
  for (i <- 1 to kertaa) algoritmi
}
var x = 0
val k = 2
toista(x+=k, 5)
println(x)                      // 10

var y = 7
val m = 9
toista( {y=m+y; println(y)}, 5) //  16
                                //  25
                                //  34
                                //  43
                                //  52
Kun toista-funktio kutsuu muodollista parametriaan algoritmi, käydään vastaavan todellisen parametrin koodi siis suorittamassa siinä ympäristössä, jossa tuo koodi annettiin.

Luennolla kerrotaan (ja piirretään) mitä kaikki tämä tarkoittaa ohjelman suoritusaikaisessa mallissa, miten tämä on mahdollista. Selitys ei kuulu "koealueeseen", mutta ehkä joillekin selventää asiaa. Itselleni tuon mallin tunteminen on ollut ratkaisevaa tämä tyyppisten ohjelmarakenteiden ymmärtämiselle.


Toistuva parametri -- vaihtelevan mittainen parametrilistan häntä

Scalaan on salakuljetettu myös Javan (>=1.5) varargs-tekniikka. Tyylikkäästi vai ei? No kyllä aika ...

Parametrilistan viimeinen ja vain viimeinen parametri voi olla vaihtelevanmittainen. Todellisia parametreja voi olla nolla tai enemmän. Kaikkien tyyppi on sama. Esimerkki:

def tulosta(kaikki: String*) {
  for (yksiMonista <- kaikki)
    print(yksiMonista +"/")
  println
}

tulosta()
tulosta("kissa")
tulosta("kissa", "hiiri")
tulosta("kissa", "hiiri", "koira")
tulosta("kissa", "hiiri", "koira", "kani")

/* Tulostus:

   kissa/
   kissa/hiiri/
   kissa/hiiri/koira/
   kissa/hiiri/koira/kani/
*/
Funktion sisällä vaihtelevan mittaista parametrilistaa käsitellään taulukkona (vrt. Javan malli!). Todelliseksi parametriksi taulukko ei kelpaa. Mutta tokihan (;-) Scalasta löytyy ilmaus, jolla taulukon saa muutettua alkioidensa jonoksi:
val t = Array("apina", "ja", "gorilla")
tulosta(t: _*)                               // apina/ja/gorilla/