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äärutiiniJa 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 // 13Monet 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 tokaNä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 // 52Kun 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)