Muutettu viimeksi 8.4.2010 / 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 file
Tä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(_))
Näissä käytetään edellä opittuja paikanpitäjäparametreja.
Pidemmästikin saa toki kirjoittaa. Esimerkiksi tuo ensimmäinen
voidaan ilmaista "pitkän kaavan kautta" näin:
(fileName: String, query: String) => fileName.endsWith(query)
Ja lopulta koko "refaktoroitu" versio:
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.
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 kiltti Scala-yhteisö on toteuttanut hienon
-- predikaattifunktion parametrina saavan --
exists-aksessorin monille kokoelmatyypeille,
sovellusohjelmoija voi hoidella homman vaivatta:
def containsNeg(nums: List[Int]) = nums.exists(_ < 0)Samaan tapaan sen sijaan että askarreltaisiin algoritmisesti
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ä 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: tavallaan tuolloin parametreja korvataan vakiofunktiolla.
"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: (Tämä kohta kuuluu oikeastaan syventävien opintojen kurssille Ohjelmointikielten periaatteet!)
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
...
Peruskurssien Java-esimerkki muokattuna Scalalle:
import java.io._
val tulos = new PrintWriter("gradu.txt")
println("Kirjoittamasi teksti menee tiedostoon gradu.txt")
println("(tyhjä rivi lopettaa!)\n")
var rivi = readLine
while (rivi != "") {
tulos.println(rivi)
rivi = readLine
}
tulos.close()
println("Työt tehty!\n")
Eli tuttua tavaraa ja tuttuun Scalamaiseen tapaan vaivattomampaa kirjoitttaa.
Määritellään ensin 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 twicen käyttöesimerkit tulostavat 7.0.
Tulkki osaa kauniisti kertoa twicen tyypin "scalaksi":
scala> def twice(op: Double => Double, x: Double) = op(op(x)) twice: ((Double) => Double,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.
Kokeillaan välinettä:
withPrintWriter(
new File("tmp.txt"),
writer => { writer.println("hölökyn kölökyn")
writer.println("töttöröö")
}
)
Näkyy syntyvän 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 "kontrollirakenteiselta", Scalan kalustosta kyllä löytyy keinoja 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öö")
}
}
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 = readLine
while (rivi != "") {
writer.println(rivi)
rivi = 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
=> muodollisenFunktioparametrinKutsu
ja a'vot tai voilà, 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 tarkistuslaskennat 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 AssertionError
Nyt 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 (marinaa ja murinaa:) "miks' mun pitää kirjoittaa tuo hölmön näköinen () =>, miksen mä saa kirjoittaa":
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 AssertionError
Jo 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.
Vaan otetaanpa avuksi tuo jo alussa paljastettu rakenne " => muodollisenFunktioparametrinKutsu" ja kirjoitetaan
def myAssert(predicate: => Boolean) =
if (assertionsEnabled && !predicate)
throw new AssertionError
Nyt 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 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) // 10
Jos 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
}