Oppimateriaalin copyright © 2009 Arto Wikla. Tämän oppimateriaalin käyttö on sallittu vain yksityishenkilöille opiskelutarkoituksissa. Materiaalin käyttö muihin tarkoituksiin, kuten kaupallisilla tai muilla kursseilla, on kielletty.
(Muutettu viimeksi 16.4.2009)

Scalan funktioita currykastikkeessa

Tällä sivulla luetellaan Scalan funktioiden erilaisia ominaisuuksia ja käyttötapoja lähinnä esimerkkien avulla. [Odersky et. al. 8 & 9]

Funktio olkoon yleisnimi kaikille nimetyille aliohjelmille arvoa palauttamattomat ja muut sivuvaikutukselliset rutiinit mukaanlukien. Metodiksi kutsuttakoon funktiota, jota käytetään olio-ohjelmointityylin aksessorina.

Paikalliset funktiot - klassinen lohkorakenne

Funktion sisällä voi määritellä funktioita, jotka ovat näkyvissä (so. käytettävissä) ainoastaan paikallisesti tässä määrittelevässä funktiossa ja sen sisältämissä funktioissa. Funktiosta näkyvät ympröivän funktion kaikki tunnukset - muuttujat, parametrit, funktiot - ellei niitä ole peitetty funktion omilla tunnuksilla. Ja peittäminen siis myös on luvallista - "litteiden kielten" lohkorakenteesta poiketen:

def päärutiini {

  def apurutiiniA {

    def apuapurutiiniAA {
      println("Olen apuapurutiiniAA")
    } // end apuapurutiiniAA

    println("Olen apurutiiniA")
    apuapurutiiniAA
  } // end apurutiiniA

  def apurutiiniB {
    println("Olen apurutiiniB")
  } // end apurutiiniB

  println("Olen päärutiini")
  apurutiiniA
  apurutiiniB
}

päärutiini

Ja siis tunnusten peittäminen on Javasta ja C:stä poiketen mahdollista myös sisäkkäisissä nimeämättömissä lohkoissa:
var x=1;
{var x=20;
   {var x=300
    println(x)
   }
   println(x)
}
println(x)

First-class-funktiot ja funktioliteraalit

Funktioliteraali on ohjelmatekstiin kirjoitettu nimetön funktio (vrt. kokonaislukuliteraali, merkkijonoliteraali, ...). Esim.
   (x: Int) => x + 1
on kokonaislukuparametrinsa seuraajan palauttava nimetön funktio.

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. Scalassa (ja muissa funktionaalisissa kielissä) siis "perinnekielistä" poiketen myös funktiot ovat tällaisia.

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ää ominaisuuksia 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.

Ja funktiot siis ovat funktioita matematiikkaa lavaeammassa merkityksessä; sivuvaikutukset yms. ovat sallittuja:

var apua = 0

increase = (x: Int) => {
  apua = 13                 // ... sivuvaikutuksia ...
  println("Kuinkas")
  println("nyt")
  println("käy?")
  x + 666
}
println(increase(10))
println(apua)          // Kuinkas
                       // nyt
                       // käy?
                       // 676
                       // 13
Monet Scalan valmiit kirjastorutiinit on toteutettu siten, että parametrina annetaan funktioarvo - tyypillisesti muttei välttämättä funktioliteraalina. Esim. kaikille 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 tulevat 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 tyyppipäättelyn yms. ansiosta usein kirjoitusvaivoja voi vähetää:
  someNumbers.filter((x) => x > 0)
// tai tässä myös:
  someNumbers.filter(x => x > 0)

Paikanpitäjäparametrit

Paikanpitäjillä "_" (placeholder) voidaan jättää antamatta edes nimiä funktioliteraalien muodollisille parametreille:
val fun = (_:String) + " ja " + (_:String)
println( fun("eka", "toka") )  // eka ja toka
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".

Osittain sovelletut funktiot

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

Esimerkiksi seuraavat ilmaukset tarkoittavat samaa:

  someNumbers.foreach(x => println(x)) // annetaan funktioliteraali
  someNumbers.foreach(println(_))      // placeholder-parametri
  someNumbers.foreach(println _)       // placeholder parametrilistan sijaan

Ilmaus println _ on esimerkki osittain sovelletusta funktiosta, "partially applied function".

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ä:

val a = sum _
println(a(1, 2, 3))
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 metodeita tai paikallisia funktioita ei nimellään saa esim. annettua parametrina, "wrappäys" -"käärepaperointi"- muuttujaan tai vakioon tekee tämän mahdolliseksi!

Edellä a:n arvoksi asetettu sum-funktio oli "osittain" sovellettu siten, ettei sitä oltu sovellettu ensimmäiseenkään parametriin... Mutta toki myös aito osittaisuus on mahdollista:

val b = sum(1, _: Int, 3)
println(b(2))
println(b(5))
Tässä b:n arvona oleva funktioarvo on tyypiltään (Int) => Int = <function>
ja sen "traitti" Function1 on tuonut mukanaan yksiparametrisen apply-aksessorin!

Jos parametrin tyyppi on funktio, kääntäjä salli 0-parametrisen "osittain" sovelletun funktion alaviivankin pois jättämisen:

someNumbers.foreach(println _)
// voidaan kirjoittaa vieläkin lyhyemmin:   ... ja taas kääntäjä päättelee...
someNumbers.foreach(println)

Sulkeumat

Sulkeuman perusidea on antaa todellisena parametrina jokin koodinpätkä muualla suoritettavaksi siten, että viittaus vastaavaan muodolliseen parametriin johtaa todellisen parametrin suorittamiseen todellisen parametrin viittausympäristössä.

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

Esimerkkinä vaikkapa 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.

Vaikka myös funktioliteraalia (x: Int) => x > 0 kutsutaan sulkeumaksi, se ei 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 parametrina annettavan koodin paikalliset tunnukset ovat sidottuja: (x: Int) => {val böö = -6; x > böö}.

Mutta kun funktioliteraaliin kijoitetaan tunnus, joille ei ole annettu merkitystä literaali-ilmauksessa, kyseessä on ns. vapaa muuttuja.
[Huom: Sana "muuttuja" tarkoittaa tässä muuttujaa matematiikan 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 + lisäys, ilmaus sisältää muuttujan (matematiikan mielessä), joka ei saa merkitystä itse ilmauksesta. Ja tässä meillä viimein on vapaa muuttuja! Ja nyt vastaa aletaan rakennella aitoja sulkeumia kiinnittämällä vapaat muuttujat (matematiikan mielessä), "sulkemalla" ne laskentarutiinin sisään!

Kokeillaan komentorivitulkilla:

scala> (x: Int) => x + lisäys
<console>:5: error: not found: value lisäys
       (x: Int) => x + lisäys
                       ^

scala> val lisäys = 10
lisäys: Int = 10

scala> (x: Int) => x + lisäys
res1: (Int) => Int = <function>
Antamalla merkitys tunnukselle lisäys, saatiin aikaan funktio-olio, jossa myös lisäys-tunnukselle oli sidottu merkitys. Ja tuo sitominen ei sitten todellakaan ole mitään arvoparametrivälitystä! Parametrina annetun sulkeuman kaikki tunnukset - myös arvon saavat muuttujat - lasketaan 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
*/

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

val someNumbers = List(-11, -10, -5, 0, 5, 10)
var sum = 0

someNumbers.foreach(sum += _)  // osittain (ei lainkaan) sovellettu funktio
println(sum)  // -11
someNumbers.foreach((x) => sum += x)
println(sum)  // -22
someNumbers.foreach({(x) => sum += x})
println(sum)  // -33
someNumbers.foreach({(x:Int) => sum += x})
println(sum)  // -44

Vaihtelevan mittainen parametrilistan häntä

Scalaan on salakuljetettu myös 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/

Currying

"Curryttaminen", "Curry-muunnos" on funktionaalisen ohjelmoinnin perusoperaatioita. Kyse on muunnoksesta, jossa funktion argumenteista poistetaan yksi muodostamalla uusi "funktio funktiolle":

Eli funktion f: (X x Y) ---> Z Curry-muunnos on curry(f): X ---> (Y ---> Z).

Tämä muunnos siis kuvaa kaksiargumenttisen funktion uudeksi funktioksi, joka kuvaa alkuperäisen ensimmäisen argumentin kuvaukseksi toiselta alkuperäiseltä argumentilta alkuperäisen funktion arvolle. Voiko sen enää selvemmin sanoa...

Käytännössä (teoriassa?) siis riittää, että käytössä on vain yksiparametrisia funktioita, koska kaikki muut funktiot voidaan Curry-muunnoksella palauttaa yksiparametrisiksi funktioiksi. Ajatelkaa mikä ilo tämä on matemaatikolle: Kun hän onnistuu todistamaan jotakin yksiargumenttisille funktioille, samalla tulee todistettua asia kaikille funktioille, joilla on äärellinen määrä argumentteja!

Ohjelmointikielillä, joissa funktiot ovat first-class-arvoja, voidaan esittää funktioarvoisia funktioita ja mm. tehdä Curry-muunnoksia. Esimerkkinä Scala-kielinen funktio ja sen Curry-muunnettu versio:

scala> def plainOldSum(x: Int, y: Int) = x + y
plainOldSum: (Int,Int)Int

scala> plainOldSum(9,5)
res0: Int = 14

scala> def curriedSum(x: Int)(y: Int) = x + y
curriedSum: (Int)(Int)Int

scala> curriedSum(9)(5)  // huom!
res1: Int = 14
Matematiikan merkinnöin plainOldSum: (Int x Int) ---> Int ja curriedSum: Int ---> (Int ---> Int).

Laskenta vastaa seuraavaa:

scala> def first(x: Int) = (y: Int) => x + y
first: (Int)(Int) => Int

scala> val second = first(5)
second: (Int) => Int = <function>

scala> second(9)
res0: Int = 14

Aiemmin jo nähtiin sellaisia osittain sovellettuja funktioita, joissa parametrien määrää supistettiin korvaamalla osa parametreista vakioarvoilla. Jos olen oikein ymmärtänyt, Curry-muunnos ei ole mitään muuta kuin osittain sovellettuja funktioita, joissa korvataan parametri funktiolla. Näin myös se aiemmin nähty "osittain sovellettujen funktioiden" -käsite on vain eräs erikoistapaus Curry-muunnoksesta.

"Curry-rakenteisista" funktioista saa helposti erikoistettua uusia funktioita:

scala> def curriedSum(x: Int)(y: Int) = x + y
curriedSum: (Int)(Int)Int

scala> val viisPlus = curriedSum(5)_    // huom. placeholder!
viisPlus: (Int) => Int = <function>

scala> viisPlus(9)
res0: Int = 14

Tyypitettyjä Lambda-lausekkeita Scalalla:

val seuraaja = (x: Int) => x + 1
val tuplattu = (x: Int) => 2*x
val neliöity = (x: Int) => x*x

val sovellus = (f:(Int) => Int, a:Int) => f(a)

println( sovellus(seuraaja,  1) )  // 2
println( sovellus(seuraaja, 20) )  // 21

println( sovellus(tuplattu, 33) )  // 66

println( sovellus(neliöity, 14) )  // 196

// jne...

Lisäesimerkkejä:

Funtio funktion paluuarvona:

def f(m:Int) = {(x:Int) => x+m}  // tyyppi päätelty f:((Int)=>Int)
println( f(4)(3) )   // 7
println( f(4)(10) )  // 14

Vielä nimiparametreista

var assertionsEnabled = true

def myAssert(predicate: () => Boolean) =
  if (assertionsEnabled && !predicate())
     throw new AssertionError

// käyttö hankalaa:

myAssert(() => 5 > 3)

// myAssert(5 > 3)  ei toimi, () => puuttuu...

// mutta jos

def byNameAssert(predicate: => Boolean) =
  if (assertionsEnabled && !predicate)
    throw new AssertionError

// jo alkaa rulata:

byNameAssert(5 > 3)

// seuraava taas olisi pöpiä:

def boolAssert(predicate: Boolean) =
  if (assertionsEnabled && !predicate)
    throw new AssertionError

boolAssert(5 > 3)

// koska ehto lasketaan arvoparametrina (ehdossa voi olla sivuvaikutuksia,
// joita ei haluta, kun assertionsEnabled = false)


Takaisin sisältösivulle.