Muutettu viimeksi 4.3.2016 / Sivu luotu 29.3.2010 / [oppikirjan esimerkit] / [Scala]
Sivun sisältöä:
Esimerkki klassisesta ohjelmointityylistä:
Laaditaan ainokaisena "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) } }Laaditaan sovellus, joka käyttää kirjastoa. Tehtävänä on tulostaa annetuista tiedostoista kaikki rivit, joiden pituus ylittää annetun pituuden. Komentoriviparametrit annetaan järjestyksessä pituus, tiedosto1, tiedosto2, ...
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 72 Rautatie.txt LongLines.scala Rautatie.txt: tullut. Vaan mitenkä? ... sitä Matti ei osannut itselleen selittää ... ja Rautatie.txt: kotoapäinhän sitä tullaan ... hyvää iltaa, ryökkynä ... terve, terve! ... Rautatie.txt: --Monienkin pyörien päällä kulki ... ja kun me seistiin siinä sen huoneen Rautatie.txt: --Vähät minä siitä, jos en näekään ... ja niinpä tuo jalkojakin pakottaa, Rautatie.txt: oikeaan käteen ja tämä tie viepi suoraan kirkolle ... on siinä punaiseksi Rautatie.txt: --Herra i--ihme! ... katso! kun on ... sehän on koreampi kuin pappilan... Rautatie.txt: --Elä he--ele--!... Siitäkö se nyt!... Hyvä isä siunatkoon ... siinäkö se Rautatie.txt: 1.C. The Project Gutenberg Literary Archive Foundation ("the Foundation" Rautatie.txt: located in the United States, we do not claim a right to prevent you from Rautatie.txt: freely sharing Project Gutenberg-tm works in compliance with the terms of Rautatie.txt: posted on the official Project Gutenberg-tm web site (www.gutenberg.net), LongLines.scala: private def processLine(filename: String, width: Int, line: String) {
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 println(filename +": "+ line) // näkyvät tänne eikä niitä tarvitse } // välittää parametreina. val source = Source.fromFile(filename) for (line <- source.getLines) processLine(line) } }
Esimerkiksi
(x: Int) => x + 1on kokonaislukuparametrinsa seuraajan palauttava nimetön funktio.
Ohjelmatekstiin kirjoitettua literaalia vastaa suoritusaikana jokin arvo. Esimerkiksi desimaalilukuliteraalia "3.14" vastaa double-liukuluvuksi tulkittava bittien jono, jne.
Funktioliteraalia puolestaan vastaa ohjelman suoritusaikana funktioarvo. Tällaisia voi sijoitella muuttujiin, välittää parametreina, käyttää funktioden paluuarvoina, jne... Tällä tavoin käytettävät arvot ovat ns. first-class-arvoja.
Useimmissa "tavallisissa" ohjelmointikielissä ainakin kokonais- ja liukuluvut ovat tällaisia. Javassa myös taulukko-oliot ovat first-class-arvoja, mutta useimmissa perinnekielissä taulukoita ei voi käyttää näin.
Scalassa (ja funktionaalisissa kielissä) "perinnekielistä" poiketen myös siis funktiot ovat first-class-arvoja. Tämä merkitsee siis mm. sitä, että funktio voidaan välittää parametrina ja että funktio voi palauttaa arvonaan funktion.
Funktioarvot on toteutettu oliona, funktio-oliona, joten niitä voi käyttää samaan tapaan 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 //Funktioarvot ovat oliota, joihin kääntäjä liittää piirretyyppejä, "mixaa traitteja", scalamaiseen tapaan:
Huom: Koska funktioarvo ja funktion arvo voivat helposti sekoittua keskenään, on turvallisempaa kutsua funktioarvoja nimellä funktio-olio!
Tuttuun tapaan funktiot 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) // 13Monet Scalan valmiit kirjastorutiinit on toteutettu siten, että parametrina annetaan funktio-olio – tyypillisesti muttei välttämättä funktioliteraalina. Myös nimettyjä funktioita voi välittää parametreina.
Esimerkiksi jo tutuksi tullut 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ää:
println(someNumbers.filter((x) => x > 0)) println(someNumbers.filter(x => x > 0))
val f = (_: Int) * (_: Int) println(f(7, 8)) // 56 val g = (_: String) + " ja " + (_: String) println( g("kissa", "koira") ) // kissa ja koiraNä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
def sum(a: Int, b: Int, c: Int) = a + b + c println(sum(1,2,3)) // 6funktiota sum sovelletaan argumentteihin 1, 2 ja 3.
Tällaisesta "kokonaan sovellettavasta" funktiosta voidaan muodostaa uusia funktioita, osittain sovellettuja funktioita, kiinnittämällä osa parametreista:
def sum(a: Int, b: Int, c: Int) = a + b + c val a = sum(2, _:Int, 4) println(a(12)) // 18 val b = sum(1, _:Int, _:Int) println(b(3,5)) // 9 val c = sum(_:Int, _:Int, _:Int) // !! println(c(1,2,3)) // 6 val d = sum _ // myös koko parametrilistan voi korvata alaviivalla! println(d(1,2,3)) // 6
Osittain sovellettu funktio (partially applied function) on siis sellainen funktio, jolle ei anneta kaikkia argumentteja – ehkä ei ensimmäistäkään.
Jos siis n:n argumentin funktion yksi argumentti kiinnitetään, saadaan (n-1):n argumentin funktio, jne.
Alaviiva "_" voi siis korvata myös kokonaisen muodollisten parametrien listan.
Tutkitaanpa näiden funktioiden tyyppejä:
scala> def sum(a: Int, b: Int, c: Int) = a + b + c sum: (a: Int, b: Int, c: Int)Int scala> val a = sum(2, _:Int, 4) a: Int => Int = <function1> scala> val b = sum(1, _:Int, _:Int) b: (Int, Int) => Int = <function2> scala> val c = sum(_:Int, _:Int, _:Int) c: (Int, Int, Int) => Int = <function3> scala> val d = sum _ d: (Int, Int, Int) => Int = <function3>
Huom: myös funktioille "sulkeiden semantiikka" on apply-metodin kutsu. Kutsun saa halutessaan myös kirjoittaa näkyviin:
def sum(a: Int, b: Int, c: Int) = a + b + c val b = sum(1, _:Int, _:Int) println(b(3,5)) println(b.apply(3,5))
Muuttuja b viittaa tässä funktioarvoon (eli funktio-olioon), joka on saanut Function2-piirretyypiltä apply-aksessorin.
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").
Seuraavat tutut ja keskenään samaa tarkoittavat ilmauksetkin saivat nyt selityksensä:
someNumbers.foreach(x => println(x)) // funktioliteraali tavalliseen tapaan someNumbers.foreach(println(_)) // funktioliteraali placeholder-parametrilla someNumbers.foreach(println _) // placeholder koko parametrilistan sijaanTässä esimerkissä "osittain" siis tarkoittaa "ei lainkaan"... Nimetystä systeemifunktiosta println muodostetaan tällä tavoin funktioliteraali.
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 ihan vakuuttunut, että tämäkin lyhennysmerkintä oli viisasta ottaa kieleen!]
Ja tarkkaan ottaen sulkeuma on siis suoritusaikainen otus! Sellainen voidaan ohjelmatekstissä määrätä syntymään (so. ohjelmoida) kirjoittamalla funktio tai funktioliteraali todelliseksi parametriksi.
Vaikka esimerkiksi funktioliteraalia (x: Int) => x > 0 voidaan kutsua sulkeumaksi, se ei kuitenkaan vielä oikeastaan "sulje" sisäänsä 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. Muuttujan x merkitys rajoittuu tähän funktioliteraaliin.
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ä itse 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} // suljettu termi res0: (Int) => Int = <function1> scala> (x: Int) => x + kasvu // avoin termi, kasvu on vapaa <console>:5: error: not found: value kasvu (x: Int) => x + kasvu ^Avoin termi ei tietenkään kelpaa kääntäjälle, koska tunnuksella kasvu ei ole mitään merkitystä tämän avoimen termin termin nimiavaruudessa. Ei esimerkiksi ole määritelty, että kasvu olisi kokonaislukumuuttuja.
Laaditaan sitten esimerkki tilanteesta, jossa sulkeumaan suljetaan vapaa muuttuja, jota päästään muokkaamaan vieraassa ympäristössä, ympäristössä, jonka nimiavaruuteen muutettava muuttuja ei kuulu:
def teeKolmesti (f: => Unit) = { // kutsuu parametriaan kolmasti f;f;f // (ei tänne mitään x:ää näy!) } def sovellus { var x = 1 def puuha() {println("töitä tehdään"); x+=1} // vapaa muuttuja x teeKolmesti(puuha) // suljetaan sulkeumaan! println(x) } sovellus // töitä tehdään // töitä tehdään // töitä tehdään // 4
Joko alkoi selvitä? Tunnuksella x on merkitys vain funktiossa sovellus; tuo merkitys on Int-muuttuja. Se on puuha-funktiolle annettavan funktioliteraalin vapaa muuttuja, joka suljetaan teeKolmesti-funktiolle parametrina annettavaan funktio-olioon.
Tällainee funktio-olio on sulkeuma.
Käytännön esimerkki sulkeumaan suljetun muuttujan arvon muuttamisesta systeemifunktion vieraassa näkyvyysaluessa on vaikkapa seuraavanlainen foreach-funktion käyttö:
val lukuja = List(3, 9, 12) var summa = 0 lukuja.foreach(summa += _) // Jokaista listan alkiota kohden käydään // kasvattamassa kutsuympäristön muuttujaa summa! println(summa) // 24Muuttuja summa ei todellakaan näy List-kokoelman foreach-aksessorin määrittelykohtaan!
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}) println(summa) // 96
Sulkeumilla on yksi ehkä vieläkin vahvempi ominaisuus: vaikka alkuperäinen viittausympäristö katoaisi – muuttujan määrittelyfunktion suoritus loppuisi – sulkeumaan suljettu muuttuja säilyy!
def teeLaskuri: (()=>Int) = { var lkm=0 return {()=>lkm+=1; lkm} } val a = teeLaskuri val b = teeLaskuri println("a = " + a()) // a = 1 println("a = " + a()) // a = 2 b(); b(); b() println("b = " + b()) // b = 4 println("a = " + a()) // a = 3 println("b = " + b()) // b = 5 println("b = " + b()) // b = 6 println("a = " + a()) // a = 4 println("b = " + b()) // b = 7 println("b = " + b()) // b = 8Tässä parametriton funktio teeLaskuri palauttaa arvonaan funktio-olion, jonka sisään on suljettu paikallinen muuttuja lkm. Vaikka tuo paikallinen muuttuja paikallisten muuttujien tuttuun tapaan häviää, kun funktion teeLaskuri suoritus päättyy, sulkeumaan suljettu versio tuosta muuttujasta säilyy! Ja kuten esimerkistä näkyy, jokaisella sulkeumalla on vieläpä oma versionsa tuosta muuttujasta!
Huom: Tässä kutsu ilman tyhjiä sulkeita tuottaa yllätyksen:
println("a = " + a) // a = <function0>
Lasketaan heti todellista parametria laskettaessa:
var arvo = "kiltti" def fun(paraFun: Unit) { println("Kutsuttu funktio alkaa, arvo = " + arvo) println("Funktio kutsuu muodollisen parametrin funktiota") paraFun println("Kutsuttu funktio päättyy, arvo = " + arvo) } println("Kutsutaan funktiota") fun( { println("Funktioliteraali asettaa muuttujan tuhmaksi") arvo = "tuhma" } )Tulostus:
Kutsutaan funktiota Funktioliteraali asettaa muuttujan tuhmaksi Kutsuttu funktio alkaa, arvo = tuhma Funktio kutsuu muodollisen parametrin funktiota Kutsuttu funktio päättyy, arvo = tuhmaLasketaan vasta, kun kutsuttu funktio kutsuu muodollista funktioparametriaan
var arvo = "kiltti" def fun(paraFun: => Unit) { println("Kutsuttu funktio alkaa, arvo = " + arvo) println("Funktio kutsuu muodollisen parametrin funktiota") paraFun println("Kutsuttu funktio päättyy, arvo = " + arvo) } println("Kutsutaan funktiota") fun( { println("Funktioliteraali asettaa muuttujan tuhmaksi") arvo = "tuhma" } )Tulostus:
Kutsutaan funktiota Kutsuttu funktio alkaa, arvo = kiltti Funktio kutsuu muodollisen parametrin funktiota Funktioliteraali asettaa muuttujan tuhmaksi Kutsuttu funktio päättyy, arvo = tuhmaAsia ei muuksi muutu, jos käytetään nimettyä funktiota:
var arvo = "kiltti" def ff = { println("Funktioliteraali asettaa muuttujan tuhmaksi") arvo = "tuhma" } println("Kutsutaan funktiota") fun(ff)
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 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 // 52Kun toista-funktio kutsuu muodollista parametriaan algoritmi, käydään vastaavan todellisen parametrin koodi siis suorittamassa siinä viiteympäristössä, jossa tuo koodi parametriksi annettiin.
Luennolla kerrotaan (ja piirretään) mitä kaikki tämä tarkoittaa ohjelman suoritusaikaisessa mallissa, miten tämä on mahdollista. Minulle tuon mallin tunteminen on ollut ratkaisevaa tämä tyyppisten ohjelmarakenteiden ymmärtämiselle.
Parametrilistan viimeinen ja vain viimeinen parametri voi olla vaihtelevanmittainen. Vastaavia 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/Huoh, ... mitäpä Scalasta ei löytyisi ... ;-)
def printOsamaara(osoittaja: Int, nimittaja: Int) { println(1.0 * osoittaja/nimittaja) } printOsamaara(3, 4) // 0.75 printOsamaara(nimittaja=3, osoittaja=4) // 1.3333333333333333 printOsamaara(osoittaja=3, nimittaja=4) // toki näinkin saa tehdä printOsamaara(osoittaja=3, 4) // ja näin printOsamaara(3, nimittaja=4) // ja näin printOsamaara(4, osoittaja=3) // mutta EI näin error: parameter 'osoittaja' is already specified at parameter position 1 printOsamaara(nimittaja=4, 3) // EIKÄ näin ^ one error foundMuodollisten parametrien nimen kirjoittaminen funktion kutsuun saattaa myös parantaa ohjelman luettavuutta kunhan vain parametrit on nimetty viisaasti.
Funktion määrittelyssa parametreille voidaan parametrilistassa myöa antaa oletusarvoja:
def printOsamaara(osoittaja: Int = 3, nimittaja: Int = 4) { println(1.0 * osoittaja/nimittaja) } printOsamaara() // 0.75 printOsamaara(2) // 0.5 printOsamaara(osoittaja=2) // 0.5 printOsamaara(nimittaja=5) // 0.6 printOsamaara(nimittaja=5, osoittaja=20) // 4.0 printOsamaara(4, 5) // 0.8