Muutettu viimeksi 9.3.2016 / Sivu luotu 1.4.2010 / [oppikirjan esimerkit] / [Scala]
Sivun sisältöä:
object FileMatcher { private def filesHere = (new java.io.File(".")).listFiles def filesEnding(query: String) = for (file <- filesHere; if file.getName.endsWith(query)) yield file def filesContaining(query: String) = for (file <- filesHere; if file.getName.contains(query)) yield file def filesRegex(query: String) = for (file <- filesHere; if file.getName.matches(query)) yield file }Ainoa ero noissa kolmessa tapauksessa siis on käytettävä merkkijonopredikaatti. Kaikilla on muuten sama hahmo:
def filesMatching(query: String, predikaatti) = for (file <- filesHere; if file.getName.predikaatti(query)) yield fileTämä ei kuitenkaan vielä ole ihan sitä meidän vahvasti ja staattisesti tyypitettyä Scalaamme...
Siispä kirjoitetaan predikaatin tyyppi näkyviin:
def filesMatching(query: String, matcher: (String, String) => Boolean) = { for (file <- filesHere; if matcher(file.getName, query)) yield file }Tämän avulla on sitten helppo toteuttaa nuo aloitusesimerkin operaatiot:
def filesEnding(query: String) = filesMatching(query, _.endsWith(_)) def filesContaining(query: String) = filesMatching(query, _.contains(_)) def filesRegex(query: String) = filesMatching(query, _.matches(_))Käyttöesimerkki:
for (f <- FileMatcher.filesEnding("txt")) println(f)Näissä funktioliteraaleissa käytetään edellä opittuja paikanpitäjäparametreja: ensimmäinen alaviiva vastaa ensimmäistä parametria, toinen toista. Saman saa ilmaista toki pidemmästikin:
(fileName: String, query: String) => fileName.endsWith(query)Tyyppipäättelyn ansiosta (vai syystä?) myös seuraava on mahdollinen muoto:
(fileName, query) => fileName.endsWith(query)
Koska kaikissa tapauksissa query on sama, rakaisua voidaan vielä yksinkertaistaa, "refaktoroida", hieman:
object FileMatcher { private def filesHere = (new java.io.File(".")).listFiles private def filesMatching(matcher: String => Boolean) = for (file <- filesHere; if matcher(file.getName)) yield file def filesEnding(query: String) = filesMatching(_.endsWith(query)) def filesContaining(query: String) = filesMatching(_.contains(query)) def filesRegex(query: String) = filesMatching(_.matches(query)) }Eipä tuo uudempi versio kovin paljon lyhyempi ole, mutta ehkäpä se on hivenen selkeämpi. Uusien hakutapojen lisääminen olisi myös aika suoraviivaista.
Huom: Kannattaa kiinnittää huomiota siihen, että tässä viimeisessä versiossa käytetään aitoja sulkeumia: kyselyfunktioiden parametri query suljetaan noihin funktioliteraaleihin!
Perinteinen tapa tutkia, onko listassa (tms.) vaikkapa negatiivisia lukuja, menee esimerkiksi siten, että oletetaan, ettei ole ja katsotaan alkio kerrallaan, kumoutuuko oletus:
def containsNeg(nums: List[Int]): Boolean = { var exists = false for (num <- nums) if (num < 0) exists = true exists }Koska Scalassa on predikaattifunktion parametrina saava exists-aksessori monille kokoelmatyypeille, sovellusohjelmoija voi hoidella homman vaivatta antamalla predikaattifunktion literaalina:
def containsNeg(nums: List[Int]) = nums.exists(_ < 0)Samaan tapaan, sen sijaan että kyseltäisiin esimerkiksi parittoman alkion olemassaoloa algoritmisesti, voidaan antaa parittomuuspredikaatti funktioliteraalina
def containsOdd(nums: List[Int]): Boolean = { var exists = false for (num <- nums) if (num % 2 == 1) exists = true exists }voidaan kirjoittaa tyylikkään kompaktisti:
def containsOdd(nums: List[Int]) = nums.exists(_ % 2 == 1)
Kyse on funktiomuunnoksesta, 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ä esimerkkimuunnos 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! Mahtaisiko iloa olla myös ohjelmointikielen toteuttajalle? Riittää toteuttaa vain yksiparametriset funktiot ja Curry? (Edellytys Curry-muunnoksen tekemiselle tietysti on, että funktioiden arvoina sallitaan funktioita.)
Ohjelmointikielillä, joissa funktiot ovat first-class-arvoja, voidaan esittää funktioarvoisia funktioita ja mm. tehdä Curry-muunnoksia. Esimerkkinä "tavallinen" kaksiparametrinen funktio ja sen Curry-muunnettu versio:
scala> def plainOldSum(x: Int, y: Int) = x + y plainOldSum: (x: Int, y: Int)Int scala> plainOldSum(9,5) res0: Int = 14 scala> def curriedSum(x: Int)(y: Int) = x + y curriedSum: (x: Int)(y: Int)Int scala> curriedSum(9)(5) // huom kahdet sulkeet! res1: Int = 14Matematiikan merkinnöin:
Laskenta vastaa seuraavaa:
scala> def first(x: Int) = (y: Int) => x + y first: (x: Int)Int => Int scala> val second = first(5) second: Int => Int = <function1> 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: tavallaan tuolloin parametreja korvataan vakiofunktiolla.
"Curry-rakenteisista" funktioista saa helposti erikoistettua uusia funktioita:
scala> def curriedSum(x: Int)(y: Int) = x + y curriedSum: (x: Int)(y: Int)Int scala> val viisPlus = curriedSum(5)_ // huom. placeholder! viisPlus: Int => Int = <function1> scala> viisPlus(9) res0: Int = 14
Tyypitettyjä Lambda-lausekkeita Scalalla: (Tämä kohta ehkä kuuluu oikeastaan syventävien opintojen kurssille Ohjelmointikielten periaatteet!)
val seuraaja = (x: Int) => x + 1 val tuplattu = (x: Int) => 2*x val nelioity = (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(nelioity, 14) ) // 196 // jne...Funtio funktion paluuarvona:
scala> def f(m:Int) = {(x:Int) => x+m} // tyyppi päätellään f:((Int)=>Int) f: (m: Int)Int => Int scala> f(4)(3) res0: Int = 7 scala> f(10)(4) res1: Int = 14 scala> val a = f(11) a: Int => Int = <function1> scala> a(8) res2: Int = 19
import java.io._ val tulos = new PrintWriter("gradu.txt") println("Kirjoittamasi teksti menee tiedostoon gradu.txt") println("(tyhjä rivi lopettaa!)\n") var rivi = scala.io.StdIn.readLine while (rivi != "") { tulos.println(rivi) rivi = scala.io.StdIn.readLine } tulos.close() println("Työt tehty!\n")
Määritellään sitten sormiharjoituksena uusi "kontrollirakenne" twice, joka soveltaa Double-->Double-funktiota kahteen kertaan:
def twice(op: Double => Double, x: Double) = op(op(x)) def kasvataYhdella(a: Double) = a + 1 println( twice(kasvataYhdella, 5) ) println( twice((x: Double) => {x + 1}, 5) ) println( twice((x: Double) => x + 1, 5) ) println( twice(x => x + 1, 5) ) println( twice(_ + 1, 5) )Kaikkki nämä twice-funktion käyttöesimerkit tulostavat 7.0. Jokaisessa on annettu tavalla tai toisella funktio, joka palauttaa parametrinsa arvon yhdellä kasvatettuna. Tulkki osaa kauniisti kertoa twicen tyypin "scalaksi":
scala> def twice(op: Double => Double, x: Double) = op(op(x)) twice: (op: Double => Double, x: Double)Double
Laaditaan sitten väline, jota käyttäen "tekstitiedoston sulkeminen ei pääse unohtumaan":
import java.io._ def withPrintWriter(file: File, op: PrintWriter => Unit) { val writer = new PrintWriter(file) try { op(writer) } finally { writer.close() } }Funktiolle withPrintWriter siis annetaan parametrina "tiedostokahva" (Javan File-olio) ja funktio, joka kuvaa PrintWriter-olion algoritmille ("Unit-funktiolle"), joka tavalla tai toisella kirjoittelee tiedostoon. Jos tulee poikkeuksia, niistä huolehtii (halutessaan) withPrintWriter-funktion kutsuja. Mutta withPrintWriter itse avaa varsinaisen tiedoston ja varmistaa joka tapauksessa, että sekä onnistuneen että poikkeukseen päättyvän operaation jälkeen tiedosto varmasti suljetaan.
Kokeile välinettä:
withPrintWriter( new File("tmp.txt"), writer => { writer.println("hölökyn kölökyn") writer.println("töttöröö") } )Pitäisi syntyä haluttu tiedosto.
Huom: Tässäkin tyyppipäättelijä helpottaa (vai vaikeuttaa?) ohjelmoijan elämää! Tuon funktioliteraalin saa kirjoittaa pidemmässäkin muodossa:
(writer: PrintWriter) => { writer.println("hölökyn kölökyn") writer.println("töttöröö") }
Jos tuo oman "kontrollirakenteen" kutsu withPrintWriter(file, algoritmi) ei vielä näytä tarpeeksi "kontrollirakenteelta", Scalan kalustosta kyllä keinoja löytyy tähänkin lähtöön...
Ensinnäkin: Yksiparametrisen funktion kutsussa todellinen parametri voidaan kaarisulkeiden sijaan ympäröidä myös aaltosulkeilla:
scala> println{"Onpa hienoa!"} Onpa hienoa!
Toiseksi: vaikka withPrintWriter olikin kaksiparametrinen, ei hätää, tehdään Curry-muunnos:
def withPrintWriter(file: File)(op: PrintWriter => Unit) { val writer = new PrintWriter(file) try { op(writer) } finally { writer.close() } }Ja nyt withPrintWriterin käyttö näyttää seuraavalta:
withPrintWriter( new File("tmp.txt") ) { writer => { writer.println("hölökyn kölökyn") writer.println("töttöröö") } }
Ehkäpä nuo aaltosulkeet vaikuttavat luontevilta parametrina annettavan funktioliteraalin ympärillä?
Toteutetaan lopuksi alun tulostustiedostoesimerkki tällä uudella, hienolla ja ikiomalla kontrollirakenteella:
import java.io._ def withPrintWriter(file: File)(op: PrintWriter => Unit) { val writer = new PrintWriter(file) try { op(writer) } finally { writer.close() } } withPrintWriter( new File("gradu.txt") ) { writer => { println("Kirjoittamasi teksti menee tiedostoon gradu.txt") println("(tyhjä rivi lopettaa!)\n") var rivi = scala.io.StdIn.readLine while (rivi != "") { writer.println(rivi) rivi = scala.io.StdIn.readLine } println("Työt tehty!\n") } }
withPrintWriter( new File("tmp.txt"), writer => { writer.println("hölökyn kölökyn") writer.println("töttöröö") } )Tai pidemmässä muodossa:
(writer: PrintWriter) => { writer.println("hölökyn kölökyn") writer.println("töttöröö") }
Jos haluaa tehdä "enemmän oikean näköisiä" kontrollirakenteita eikä parametreja tarvita, voi tehdä toisinkin. Muodollisen funktioparametrin parametrilistan voi jättää tyhjäksi
=> muodollinenFunktioparametrija jo alkavat omat välineet muistuttaa oikeita kontrollirakenteita...
Ensin kirjan esimerkki lyhyesti ja muokattuna: Laaditaan oma väittämien tarkistusväline. Ideana on että ohjelman testausvaiheessa väittämät tarkistetaan, mutta tuotantoversiosta nämä mahdollisesti raskaat tarkistukset jätetään pois. Ja senhän tietää, mitä siitä sitten seuraa...;-)
Ohjelmoidaan väittämän tarkistusfunktio:
var assertionsEnabled = true def myAssert(predicate: () => Boolean) = if (assertionsEnabled && !predicate()) throw new AssertionErrorNyt erilaisiin kriittisiin ohjelmakohtiin voi kirjoitella väittämiä tyyliin:
myAssert(() => jokin väittämä muuttujien arvosta) // esim. x!=666Homma toimii hienosti: Kun assertionsEnabled==true, predikaatti lasketaan. Ja koska todellinen predikaatti on sulkeuma, se todellakin lasketaan kutsukohdan ympäristössä!
Kun assertionsEnabled==false, predikaattia ei lasketa. Muista että && on ehdollinen and: jos ensimmäinen operandi on epätosi, toista ei evaluoida!
Hienoa muuten, mutta sovellusohjelmoija voi alkaa valittaa: "Miks' mun pitää kirjoittaa tuo hölmön näköinen "() =>", miksen mä saa kirjoittaa vain tähän tyyliin:
myAssert(jokin väittämä muuttujien arvosta) // ei toimi, koska () "=>" puuttuu...
No, auttavainen systeemiohjelmoija yrittää tyydyttää sovellusohjelmoijan toiveen:
def myAssert(predicate: Boolean) = if (assertionsEnabled && !predicate) throw new AssertionErrorJo kelpaa kääntäjälle ja tulkille tuo toivottu muoto:
myAssert(jokin väittämä muuttujien arvosta)Tässä kuitenkin jouduttiin ojasta allikkoon: Tavallisen arvoparametrin tapaan tuo todellinen parametri "jokin väittämä muuttujien arvosta" evaluodaan nyt aina ennen myAssert-funktion kutsua! Riippumatta assertionsEnabled-muuttujan arvosta.
Ongelmia tulee esimerkiksi jos väittämän laskennalla on jokin sivuvaikutus, jonka ei toivota toteutuvan, kun assertionsEnabled on false. Tai seuraavan tyyppinen tilanne:
... var j = 0 ... myAssert(100/j == 0) // tulee nollalla jako ennen aikojaan...
Vaan otetaanpa avuksi tuo jo alussa paljastettu rakenne " => muodollinenFunktioparametri" ja kirjoitetaan
def myAssert(predicate: => Boolean) = if (assertionsEnabled && !predicate) throw new AssertionErrorNyt tuo toivottu muoto
myAssert(jokin väittämä muuttujien arvosta)sekä kelpaa että myös tarkoittaa sitä, mitä haluttiin!
Palataan samassa hengessä vielä edellisen luvun 8 lopun omaan toistolause-esimerkkiin:
def toista(algoritmi: =>Unit, kertaa:Int) { for (i <- 1 to kertaa) algoritmi } var x = 0; val k = 2 toista(x+=k, 5) println(x) // 10Jos tuon " =>":n jättää pois, kaikki ns. "toimii", mutta x:ää kasvatetaa vain kerran k:n arvolla! Parametri evaluoidaan arvoparametrina ennen kutsutun funktion käynnistämistä.
Jospa nyt lopuksi vähän diivailtaisiin...
Muokataan edellistä toistorakennetta vähän ja erityisesti tehdään Curry-muunnos, niin jopa alkaa näyttää omalta kontrollirakenteelta:
def toistokertoja(kertaa:Int)(algoritmi: =>Unit) { for (i <- 1 to kertaa) algoritmi } var x = 0; val k = 2 toistokertoja(5) { x+=k } println(x) toistokertoja(2) { println("Scala on kivaa?") println("vai...") } toistokertoja(3) { toistokertoja(7) { print("*") } println }