Muutettu viimeksi 1.4.2010 / Sivu luotu 29.3.2010 / [oppikirjan esimerkit] / [Scala]
Sivun sisältöä:
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),
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) } }
Esimerkiksi
(x: Int) => x + 1on 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) // 13Monet 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)
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.
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 sijaanIlmaus 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)) // 6Muuttuja 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)) // 9Saatiin 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!]
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 = 7006Kuten 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 // 52Kun 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.
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/