Funktio olkoon yleisnimi kaikille nimetyille aliohjelmille arvoa palauttamattomat ja muut sivuvaikutukselliset rutiinit mukaanlukien. Metodiksi kutsuttakoon funktiota, jota käytetään olio-ohjelmointityylin aksessorina.
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)
(x: Int) => x + 1on 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)
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".
Esimerkiksi seuraavat ilmaukset tarkoittavat samaa:
someNumbers.foreach(x => println(x)) // annetaan funktioliteraali someNumbers.foreach(println(_)) // placeholder-parametri someNumbers.foreach(println _) // placeholder parametrilistan sijaanIlmaus 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>
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)
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
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/
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 = 14Matematiikan 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 = 14Aiemmin 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
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.