Kurssin viimeisimmän version löydät täältä: O1: 2024
Luku 6.2: Nimettömiä funktioita
Tästä sivusta:
Pääkysymyksiä: Miten korkeamman asteen funktioita olisi kätevämpi käyttää? Miten kirjoitan ohjelmaan kertakäyttöisiä funktioita?
Mitä käsitellään? Funktioliteraalit ja nimettömät funktiot. Lyhennysmerkintöjä funktioliteraaleille.
Mitä tehdään? Luetaan ja tehdään pikkutehtäviä.
Suuntaa antava työläysarvio:? Pari tuntia.
Pistearvo: A35 + B5 + C5.
Oheisprojektit: HigherOrder. AuctionHouse1-projektia hipaistaan.
Johdanto: literaaleista yleisesti
Tässä luvussa et opi kirjoittamaan kokonaan uudenlaisia ohjelmia vaan uudenlaisia merkintätapoja funktioille. Nämä merkintätavat kätevöittävät ohjelmien laatimista huomattavasti.
Aloitetaan kuitenkin lukuarvoista.
Jo kurssin ensimmäisellä viikolla opittiin käyttämään muuttujia lukuarvojen tallentamiseen. Muuttujan nimellä voi viitata tiettyyn lukuun, mikä on kätevää, kun samaan arvoon halutaan viitata useasta eri kohdasta ohjelmakoodissa.
Toisaalta esimerkiksi lukuarvoihin ei aina viitata muuttujien nimillä. Jos haluamme
korottaa var
-muuttujan indeksi
arvoa yhdellä silmukassa, niin emme yleensä kirjoita
tuota korotusta näin:
val korotus = 1
indeksi += korotus
Kirjoitamme tuon sijaan yksinkertaisesti indeksi += 1
käyttäen kokonaislukuliteraalia 1
.
Literaalihan on ohjelmakoodiin kirjoitettu lauseke, joka ilmaisee tietyn arvon
"suoraan" tai "kirjaimellisesti". Esimerkkikäskyssämme käytämme literaalia, koska:
- ihmislukija pystyy helposti mieltämään "yhdellä kasvattamisen" yhdeksi selkeäksi kokonaisuudeksi;
- literaalin
1
merkityksen käskyssäindeksi += 1
näkee tuosta käskystä vaivattomasti, jotenkorotus
-muuttujan nimi ei selkiytä ohjelmaa; korotus
-muuttujaa oletettavasti tarvittaisiin vain yhdessä käskyssä; arvo1
on tässä "kertakäyttöinen";- kun
korotus
-muuttujaa ei ole, niin koodia lukevan ihmisen ei tarvitse suoda ajatustakaan sille, onko muuttujalla jokin muukin merkitys koodissa kuin sen käyttö rivilläindeksi += korotus
; ja - literaalia käyttävä ohjelmakoodi on siis lyhyempi olematta vaikeaselkoisempi (pikemminkin päinvastoin).
Siirrytään luvuista funktioihin.
Jo kurssin alussa olet oppinut määrittelemään funktioita, joilla on nimi. Funktion
nimellä, joka määritellään def
-sanan yhteydessä, voit viitata tiettyyn toimenpiteeseen
(vrt. muuttujan nimellä voi viitata lukuun). Tämä on erityisen kätevää, kun sama
toimenpide halutaan määrätä suoritettavaksi useassa eri ohjelmakoodin kohdassa.
Tässä luvussa näet, että voimme myös määritellä nimettömiä, "kertakäyttöisiä" funktioita.
Siihen tarvitsemme funktioliteraaleja.
Funktioliteraalit
Luvussa 6.1 oli esillä seuraava funktio, joka kutsuu parametriksi annettua funktiota kahdesti:
def kahdesti(toiminto: Int => Int, kohde: Int) = toiminto(toiminto(kohde))
Tätä funktiota käytettiin luvun kasvattamiseen kahdesti ja luvun tuplaamiseen kahdesti. Niinpä määriteltiin tällaiset pikkufunktiot:
def seuraava(luku: Int) = luku + 1
def tuplaa(tuplattava: Int) = 2 * tuplattava
Käytimme funktioita luvussa 6.1 näin:
kahdesti(seuraava, 1000)res0: Int = 1002 kahdesti(tuplaa, 1000)res1: Int = 4000
Saman voi tehdä helpomminkin. Olkoon kahdesti
-funktio määritelty kuten edellä. Sen
sijaan seuraava
- ja tuplaa
-funktioita emme tarvitse, kun määrittelemme parametriksi
välitettävät funktiot "lennosta". Aloitetaan kasvattamalla kahdesti yhdellä:
kahdesti(luku => luku + 1, 1000)res2: Int = 1002
Tässä taas kahdesti tuplaaminen nimetöntä funktiota käyttäen:
kahdesti(n => 2 * n, 1000)res3: Int = 4000
n
sijaan toki olla myös nimenä tuplattava
tms.Funktioliteraalimerkinnöistä
Nuolesta: Funktioliteraalin merkitsemisessä käytetään samanlaista nuolta, jollaista
käytetään funktioarvojen tyyppien kuvaamiseen (esim. kahdesti
-funktiossahan on
parametrin tyypiksi merkitty kaksoispisteen perään Int => Int
).
Parametrien tyypeistä: Funktioliteraalien parametrien tyyppejä ei usein tarvitse
kirjoittaa, koska ne ovat pääteltävissä käyttöyhteydestä. Esimerkiksi yllä ei ollut
pakko erikseen määritellä, mikä on parametrin luku
tietotyyppi literaalissa
luku => luku + 1
. On automaattisesti pääteltävissä, että kyseessä on Int
-arvo,
koska tuo literaali välitetään parametriksi kahdesti
-funktiolle, joka edellyttää,
että sille annetaan nimenomaan Int => Int
-tyyppinen funktioparametri.
On siis yleistä, että funktioliteraalien parametrimuuttujien tyypit ovat automaattisesti
pääteltävissä. Silloin, kun ne eivät ole, ne voi merkitä aivan samalla tavalla kuin
nimettyihinkin funktioihin. Tällöin on käytettävä sulkuja ja kaksoispistettä. Esimerkiksi
literaalin luku => luku + 1
voi kirjoittaa "auki" näin: (luku: Int) => luku + 1
.
Suluista: Sulut voi jättää pois funktioliteraalissa mainitun parametrin ympäriltä silloin, kun funktio ottaa yhden parametrin eikä sen tyyppiä ole erikseen mainittu. Näinhän teimme esimerkeissämme ylempänä. Kun parametreja on monta, ovat sulut pakolliset (mistä esimerkkejä alla). Sulut saa kyllä aina kirjoittaa.
Nimetön funktio arvona
Tutkitaan vielä REPLissä sitä, miten funktioliteraalit määrittelevät nimettömiä funktio-olioita.
Tässä ensin yhdelläkasvatusliteraali:
(luku: Int) => luku + 1res4: Int => Int = <function1>
Int => Int
.Int
määritteleminen erikseen on tässä
REPL-esimerkissä pakollista, koska tästä asiayhteydestä ei ole
muuten selvää, mikä tyyppi on kyseessä. (Tyyppi voisi muuten olla
myös esimerkiksi Double
tai String
.)Seuraavan literaalin arvo puolestaan on kaksiparametrinen funktio, joka palauttaa
parametriensa pyöristetyn osamäärän. Se on tyyppiä (Double, Double) => Int
:
(x: Double, y: Double) => (x / y).round.toIntres5: (Double, Double) => Int = <function2>
Viittaukset nimettömään funktioon
Funktioliteraalin määrittelemä funktio on olio. Sitä voi käsitellä kuten muitakin olioita.
Funktio itse on nimetön, mutta muuttujaan voi sijoittaa viittauksen funktio-olioon:
val kokeilufunktio = (x: Double, y: Double) => (x / y).round.toIntkokeilufunktio: (Double, Double) => Int = <function2>
Tällöin funktiota voi kutsua muuttujan nimeä käyttäen:
kokeilufunktio(10, 4)res6: Int = 3
Voidaan myös määritellä toinen muuttuja, joka viittaa samaan funktioon:
val pyoristaOsamaara = kokeilufunktiopyoristaOsamaara: (Double, Double) => Int = <function2>
Tällöin funktiota voi kutsua myös tämän toisen muuttujan nimellä:
pyoristaOsamaara(100, 8)res7: Int = 13
Funktiot olioina
Luvussa 5.3 näit, että Scala-olioille voi määritellä apply
-nimisen
metodin, joka toimii eräänlaisena "olion oletusmetodina". Esimerkiksi
kutsu jokuOlio(parametrit)
on lyhennysmerkintä kutsulle
jokuOlio.apply(parametrit)
. Oliota, jolla on apply
-metodi, voi
näin käyttää "vähän kuin se olisi funktio".
Kaikilla Scalan funktio-olioilla on apply
-metodi, ja seuraava rivi
tekee saman kuin edellinenkin esimerkki:
pyoristaOsamaara.apply(100, 8)res8: Int = 13
Olionäkökulmasta ajateltuna funktion kutsuminen on siis funktio-olion
apply
-metodin kutsumista.
Miksi funktioliteraaleja ja nimettömiä funktioita?
Funktioliteraalien hyvät ja huonot puolet ovat samansuuntaisia kuin vaikkapa kokonaislukuliteraalienkin. Eräitä hyviä puolia ovat:
- Ei tarvitse määritellä nimiä jokaiselle eri pikkufunktiolle, jota käytetään vain kerran.
- Nimettömän funktion määrittelyn voi kirjoittaa juuri siihen kohtaan
koodissa, jossa funktiolla tehdään jotain (esim. kohtaan, jossa se
välitetään
kahdesti
-funktiolle parametriksi). Tämä voi helpottaa koodin lukemista erityisesti silloin, kun nimetön funktio on lyhyt. - Nimettömiä funktioita käyttämällä koodia voi saada lyhemmäksi. Lyhyys ei ole itsetarkoitus mutta mukavaa silloin, kun se ei hankaloita lukemista.
Heikkouksia ja rajoituksia:
- Nimettömillä funktioilla ei ole nimiä, mikä voi tilanteesta riippuen tehdä ohjelmasta hankalaselkoisemman.
- Funktioliteraalien kirjoittaminen ja lukeminen voivat olla aluksi haastavia (mutta merkintätapoihin kyllä tottuu).
- Monille funktioille tarvitaan nimet (esim. olioiden julkisille metodeille), joten nimettömät funktiot eivät kelpaa kaikkeen.
- Jos haluamme kutsua samaa nimetöntä funktiota useasti, niin on kuitenkin määriteltävä muuttuja (siis nimi), jonka kautta se onnistuu.
Nimettömiä funktioita täytyy käyttää harkiten. Yksi oivallinen käyttö on pienten kertakäyttöfunktioiden välittäminen parametreiksi toiselle funktiolle kuten yllä.
Nimettömien funktioiden käyttö on mm. Scala-ohjelmoinnissa erittäin yleistä, ja siihen kannattaa totuttautua. Myös tällä kurssilla niitä käytetään jatkossa paljon.
Termi: lambda
Funktioliteraaleja kutsutaan usein lambdalausekkeiksi (lambda
expression) ja nimettömiä funktioita lambdafunktioiksi (lambda
function). Sanaa "lambda" käytetään useassa ohjelmointikielessä myös
funktioliteraalien määrittelemiseen; esimerkiksi Python-kielinen lauseke
lambda luku: luku + 1
vastaa Scalan lauseketta luku => luku + 1
.
Lambda-sanan käytölle tässä yhteydessä on historialliset syyt. Nämä
lausekkeet ovat peräisin matemaatikko Alonzo Churchin kehittämästä
ja tietojenkäsittelytieteen kehitykseen käänteentekevästi vaikuttaneesta
lambdakalkyylistä.
Tuon matemaattisen mallin merkintätapoihin kreikan aakkonen lambda
taas päätyi lähes kirjaimellisesti hatusta vetämällä: Church
kirjoitti luku => luku + 1
sijaan ŷ.y+1
eli käytti ns. hattua kirjaimen päällä,
mutta tuo ei ilmeisesti
1930-luvun latojalta sujunut, joten tämä nykäisi hattua hieman
vasemmalle: ^y.y+1
. Siitä se sitten luettiin lambda-kirjaimeksi Λ.
Funktioliteraaleja käytössä
Useat luvun 6.1 esimerkeistä voi kirjoittaa näppärästi uusiksi funktioliteraaleilla. Tarkastellaan muutamaa tapausta.
Esimerkki: tabulate
Käytimme tabulate
-funktiota vektorin alustamiseen näin:
Vector.tabulate(10)(tuplaa)res9: Vector[Int] = Vector(0, 2, 4, 6, 8, 10, 12, 14, 16, 18)
Sama hoituu kätevästi nimettömällä funktiolla ilman erillistä summafunktiota:
Vector.tabulate(10)(indeksi => 2 * indeksi)
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Välikommentti -lyönneistä
Välilyönnit funktioliteraalien ympärillä joskus selkiyttävät ohjelmakoodia, kun parametrifunktion määrittelevä koodinpätkä erottuu selvemmin.
Vector.tabulate(10)( indeksi => 2 * indeksi )
Kurssimateriaalissa käytetään välilyöntejä tähän tapaan usein. Voit itsekin tehdä niin oman harkintasi mukaan.
Esimerkkejä: onkoJarjestyksessa
ja useita parametreja
Luvussa 6.1 laadittiin tämä korkeamman asteen funktio:
def onkoJarjestyksessa(eka: String, toka: String, kolmas: String, vertaa: (String, String) => Int) =
vertaa(eka, toka) <= 0 && vertaa(toka, kolmas) <= 0
Ja nämä sen parametriksi sopivat vertailufunktiot:
def vertaaPituuksia(jono1: String, jono2: String) = jono1.length - jono2.length
def vertaaIntArvoja(jono1: String, jono2: String) = jono1.toInt - jono2.toInt
def vertaaMerkkeja(jono1: String, jono2: String) = jono1.compareToIgnoreCase(jono2)
Niitä käytettiin esimerkiksi näin:
onkoJarjestyksessa("Java", "Scala", "Haskell", vertaaPituuksia)
onkoJarjestyksessa("Java", "Scala", "Haskell", vertaaMerkkeja)
onkoJarjestyksessa("200", "123", "1000", vertaaIntArvoja)
Funktioliteraalien avulla viimeksi mainitut rivit voisi kirjoittaa myös näin, ilman että tarvitsemme erillisiä vertaamisfunktioiden määrittelyjä:
onkoJarjestyksessa("Java", "Scala", "Haskell", (j1, j2) => j1.length - j2.length )
onkoJarjestyksessa("Java", "Scala", "Haskell", (j1, j2) => j1.compareToIgnoreCase(j2) )
onkoJarjestyksessa("200", "123", "1000", (j1, j2) => j1.toInt - j2.toInt )
Sulut pitää muistaa.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Esimerkki: findAll
ja metodikutsu funktioparametrissa
Luvussa 6.1 AuctionHouse
-luokalle laadittiin findAll
-metodi, jolla voi hakea esineitä
parametrifunktiona kuvatun kriteerin perusteella.
class AuctionHouse {
private val items = Buffer[EnglishAuction]()
def findAll(checkCriterion: EnglishAuction => Boolean) = {
val found = Buffer[EnglishAuction]()
for (currentItem <- this.items) {
if (checkCriterion(currentItem)) {
found += currentItem
}
}
found.toVector
}
// ... muita metodeita ...
}
Sitä käytettiin kokeeksi näin:
object FindAllTest extends App {
def checkIfOpen(candidate: EnglishAuction) = candidate.isOpen
def checkIfHandbag(candidate: EnglishAuction) = candidate.description.toLowerCase.contains("handbag")
val house = new AuctionHouse("ReBay")
house.addItem(new EnglishAuction("A glorious handbag", 100, 14))
house.addItem(new EnglishAuction("Collectible Easter Bunny China Thimble", 1, 10))
println(house.findAll(checkIfOpen)) // finds both auctions
println(house.findAll(checkIfHandbag)) // finds only the first auction
}
Miten toteuttaisit FindAllTest
-ohjelman nimettömillä funktioilla? Oletetaan siis, että
nimettyjä funktioita checkIfOpen
ja checkIfHandbag
ei ole. Miten kirjoittaisit kaksi
viimeistä riviä, joilla findAll
-metodia kutsutaan, funktioliteraaleja käyttäen?
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Esimerkki: repeatForEachElement
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Esimerkki: kuvankäsittelyä funktioliteraaleilla
Lisää vanhaa koodia luvusta 6.1:
def blueGradient(x: Int, y: Int) = Color(0, 0, x.toDouble / (size - 1) * Color.Max)blueGradient: (x: Int, y: Int)o1.gui.Color Pic.generate(size, size, blueGradient)res10: Pic = generated pic
Nuo käskyt voi korvata tällä yhdellä:
Pic.generate(size, size, (x, y) => Color(0, 0, x.toDouble / (size - 1) * Color.Max) )res11: Pic = generated pic
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Entä tämä kuvangenerointiesimerkki?
def artwork(x: Int, y: Int) = if (x * x > y * 100) Red else if (x + y < 200) Black else if (y % 10 < 5) Blue else Whiteartwork: (x: Int, y: Int)Color Pic.generate(size, size * 2, artwork)res12: Pic = generated pic
Mitä monimutkaisemmaksi funktioliteraali muodostuu, sitä epätodennäköisempää on, että sellaisen käyttö selkiyttää ohjelmaa. Esimerkiksi tämä on jo aika mutkikas, kirjoitti sen sitten yhdelle tai monelle riville.
Pic.generate(size, size * 2, (x, y) => if (x * x > y * 100) Red else if (x + y < 200) Black else if (y % 10 < 5) Blue else White )res13: Pic = generated pic
Välisana
Nyt osaat kirjoittaa funktioliteraaleja, joilla määritellään nimettömiä funktioita. Nimettömiä funktioita esiintyy hieman eri muodoissa monissa eri ohjelmointikielissä muttei kaikissa. Niitä käytetään erityisen runsaasti funktionaalisessa ohjelmoinnissa, josta kerrotaan lisää luvussa 10.2.
Tämän luvun loppuosa liittyy lähinnä Scala-kieleen. Opit toisen tavan kirjoittaa funktioliteraaleja Scalalla.
Nimettömiä parametreja
Monesti pientä funktioliteraalia kirjoittaessa ei oikeastaan ole paljonkaan väliä, minkä
nimen parametrimuuttujalle laittaa. Voi kirjoittaa esimerkiksi luku => luku + 1
tai
x => x + 1
tai muuta vastaavaa, ja lukijalle käy joka tapauksessa ilmi, että kyseessä
on parametriarvoaan yhtä suuremman luvun palauttava funktio.
Siksi Scala tarjoaa mahdollisuuden jättää nimen määrittelemättä kokonaan ja samalla kirjoittaa funktioliteraaleja entistä tiivimmässä muodossa.
Tarkastellaan taas literaalia luku => luku + 1
, jonka oleellinen sisältö on, että
johonkin annettuun arvoon lisätään ykkönen. Voimme määritellä nimettömän funktion siten,
että keskitymme vain tähän oleellisimpaan sisältöön. Täsmälleen samanlaisen funktion
saa määriteltyä näin lyhyellä merkinnällä:
_ + 1
Alaviiva tarkoittaa tässä nimetöntä parametria. Koska alaviivoja on literaalissa yksi, kyseessä on yksiparametrinen funktio. Parametriarvo lasketaan yhteen ykkösen kanssa, ja lopputulos on nimettömän funktion palauttama arvo.
Tässä lyhyemmässä ilmaisutavassa siis ei tarvita oikealle osoittavaa nuoltakaan lainkaan. Pelkkä alaviiva riittää kertomaan, että kyseessä on funktioliteraali.
Lyhennettyjä funktioliteraaleja voi käyttää vaikkapa näin:
kahdesti( _ + 1 , 1000) // palauttaa 1002
kahdesti( 2 * _ , 1000) // palauttaa 4000
Ylempänä lueteltiin nimettömien funktioiden hyviä ja huonoja puolia verrattuna nimettyihin funktioihin. Lyhennettyä "alaviivanotaatiota" käyttäessä nämä hyvyydet ja huonoudet korostuvat entisestään. Hyvällä maulla käytettyinä myös lyhennetyt funktioliteraalit voivat parantaa koodin luettavuutta. Aluksi ne voivat näyttää liki maagisilta, mutta niihin tottuu harjoittelun myötä nopeasti.
Parametrien tyypeistä
Kuten lyhentämättömienkin funktioliteraalien tapauksessa myös lyhennetyistä funktioliteraaleista saa usein jättää parametrien tyypit pois, koska ne ovat pääteltävissä käyttökontekstista. Näin on tehty kaikissa yllä olevissa esimerkeissä.
Parametrien tyypit voi myös mainita erikseen. Esimerkiksi äsken mainittu literaali _ + 1
voitaisiin kirjoittaa myös (_: Int) + 1
Kuitenkin tyyppien merkitseminen nimettömiin
parametreihin helposti tekee koodista vaivalloista luettavaa, eikä tätä tapaa käytetä
tämän kurssin materiaalissa.
Useita nimettömiä parametreja
Nimettömällä funktiolla voi olla useitakin nimettömiä parametreja. Tällöin ensimmäinen funktioliteraaliin kuuluva alaviiva viittaa ensimmäiseen funktiolle välitettyyn parametriarvoon, toinen toiseen ja niin edelleen.
Esimerkiksi nämä käskyt ovat täysin toisiaan vastaavat:
Vector.tabulate(3, 5)( (rivi, sarake) => rivi + sarake )
Vector.tabulate(3, 5)( _ + _ )
Samoin nämä yllä nähdyt funktioliteraalit...
onkoJarjestyksessa("Java", "Scala", "Haskell", (j1, j2) => j1.length - j2.length )
onkoJarjestyksessa("Java", "Scala", "Haskell", (j1, j2) => j1.compareToIgnoreCase(j2) )
onkoJarjestyksessa("200", "123", "1000", (j1, j2) => j1.toInt - j2.toInt )
... voi kirjoittaa näinkin:
onkoJarjestyksessa("Java", "Scala", "Haskell", _.length - _.length )
onkoJarjestyksessa("Java", "Scala", "Haskell", _.compareToIgnoreCase(_) )
onkoJarjestyksessa("200", "123", "1000", _.toInt - _.toInt )
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Vielä vähän merkintätapojen vertailua
Ajatellaan tilannetta, jossa meillä on alkiokokoelma, jonka kunkin alkion haluamme tulostaa. Tässä on kolme suomenkielistä käskyä:
- Pisin: "Toista jokaiselle alkiolle: tulosta x, missä x on nyt käsiteltävä alkio."
- Keskipitkä: "Toista jokaiselle alkiolle: tulosta kyseinen alkio."
- Ytimekäs: "Toista jokaiselle alkiolle: tulosta."
Suomeksi viestittäessä valinta tällaisten käskyjen välillä voidaan tehdä tilanteen ja oman maun mukaan.
Scala on ohjelmointikieleksi joustava, ja voimme antaa vastaavan käskyn Scalallakin eri
tavoilla. Kun käytössä on luvun 6.1 repeatForEachElement
-funktio ja lukuja sisältävä
vektori, niin kaikki seuraavista tekevät saman asian:
- Pisin:
repeatForEachElement(vektori, x => println(x) )
- Keskipitkä:
repeatForEachElement(vektori, println(_) )
- Ytimekäs:
repeatForEachElement(vektori, println)
Kaksi jälkimmäistä ovat käytännössä vain lyhempiä tapoja kirjoittaa ensimmäinen, pisin versio.
Tässä toinen esimerkki, jossa oletetaan, että olio
viittaa johonkin sellaiseen olioon,
jonka metodi nimeltä toimi
ottaa parametrikseen kokonaisluvun:
- Pisin:
repeatForEachElement(vektori, alkio => olio.toimi(alkio) )
- Keskipitkä:
repeatForEachElement(vektori, olio.toimi(_) )
- Ytimekäs:
repeatForEachElement(vektori, olio.toimi)
Kun mikä tahansa ilmaisuista toimii, valinnan voi tehdä koodin luettavuuden perusteella.
Riippuu tilanteesta ja lukijasta, mikä merkintätavoista on paras. Ytimekkäimmässä tavassa
on vain välttämätön; sen edut lienevät suurimmillaan, kun parametriksi välitetty funktio
on todella tuttu (kuten println
on). Siihen verrattuna keskipitkä tapa korostaa
esimerkkiemme lukijalle sitä, että kukin alkioista tulee vuoron perään käytetyksi
parametriarvona: se käy suoremmin ilmi ilmaisusta olio.toimi(_)
kuin ilmaisusta
olio.toimi
, joka irrallaan tarkasteltuna näyttää parametrittoman metodin kutsulta.
Monisanaisin tapa alleviivaa parametrimuuttujaa ja mahdollistaa sen nimeämisen.
Kaikkia kolmea ilmaisutapaa esiintyy tämän kurssin materiaalissa ja muissa Scala-ohjelmissa. Onkin hyvä tuntea ne kaikki, vaikka omissa ohjelmissasi suosisitkin jotakin niistä.
Ohjenuora
Voit itse käyttää aina pitkiä funktioliteraaleja, joissa parametrit on nimetty, kunnes alkaa tuntua siltä, että haluaisit kirjoittaa lyhyemmin.
def
ja funktion sijoittaminen muuttujaan
Olkoon määriteltynä seuraava
-funktio def
-sanaa käyttäen:
def seuraava(n: Int) = n + 1seuraava: (n: Int)Int
Tässä luvussa on nähty, että funktioliteraalin voi sijoittaa
muuttujaan, jonka jälkeen sitä saattoi kutsua muuttujan nimellä.
Luonnolliselta tuntuisi, että myös nimettyä funktiota seuraava
voisi käyttää vastaavasti. Harmiksemme huomaamme, ettei se
ilmeisimmällä tavalla onnistu:
val kokeilu = seuraava <console>:15: error: missing argument list for method seuraava Unapplied methods are only converted to functions when a function type is expected. You can make this conversion explicit by writing `seuraava _` or `seuraava(_)` instead of `seuraava`. val kokeilu = seuraava ^
Saimme valituksen siitä, että tuossa ei ollut annettu
seuraava
lle parametreja. Toisin kuin tarkoitimme,
Scala-kääntäjä tulkitsi ilmaisun yritykseksi kutsua seuraava
a,
jonka palauttama kokonaisluku olisi sitten sijoitettu
kokeilumuuttujaan (joka saisi tyypikseen Int
).
Saimme myös ehdotuksen käyttää alaviivaa. Vastaava sijoitus onnistuukin pienellä tempulla.
val kokeilu = seuraava(_)kokeilu: Int => Int = <function1> kokeilu(100)res14: Int = 101
Itse asiassa myös sulut voi tällaisessa tilanteessa jättää pois, jolloin alaviiva korvaa koko parametriluettelon:
val kokeilu = seuraava _kokeilu: Int => Int = <function1>
Kun riittävä tyyppitieto on saatavilla, Scala-kääntäjä osaa tehdä vastaavan muunnoksen automaattisesti ilman alaviivaakin. Esimerkiksi tässä erikseen kirjataan, että kokeilumuuttujaan on tarkoitus sijoittaa kokonaislukuja käsittelevä funktio:
val kokeilu: Int => Int = seuraavakokeilu: Int => Int = <function1>
Lisätietoja Programming in Scala -kirjasta.
Nimettömien parametrien rajoituksia
Rajoitus: vain kertakäyttöisille parametreille
Tarkastellaan paria esimerkkiä, jotka paljastavat erään lyhennettyjen funktioliteraalien rajoituksen.
Seuraava funktio laskee tuloksen parametrinsa perusteella. Sen rungossa sama parametrimuuttuja esiintyy useammin kuin kerran.
def laske(luku: Double) = luku * luku + 10
Saman toiminnallisuuden voi kirjoittaa nimettömänä funktiona näin:
luku => luku * luku + 10
Sen sijaan tämä lyhennetty versio ei toimi halutusti:
_ * _ + 10
Koska kukin alaviiva vastaa erillistä nimetöntä parametria, niin äskeinen lyhennetty literaali ajaa saman asian kuin nämä pidemmät versiot:
(luku1, luku2) => luku1 * luku2 + 10
def laske(luku1: Double, luku2: Double) = luku1 * luku2 + 10
Kyseessä on siis aivan toinen, kaksiparametrinen funktio.
Kuhunkin nimettömään parametriin voi viitata vain yhdestä kohdasta funktioliteraalia; kukin alaviiva viittaa eri parametriin. Jos haluat viitata funktioliteraalin parametriin useasti, käytä lyhentämätöntä funktioliteraalia nimetyillä parametreilla.
Toinen esimerkki samasta rajoituksesta:
def swapGreenAndBlue(original: Color) = Color(original.red, original.blue, original.green)
Parametrimuuttuja original
esiintyy rungossa useasti.
swapGreenAndBlue
-funktiota vastaavan funktioliteraalin voi
muodostaa (minkä teitkin aiemmassa tehtävässä), mutta lyhempää,
alaviivallista ei voi.
Toinen rajoitus: sisäkkäiset sulut
Kun kutsut lyhennetyssä funktioliteraalissa jotakin toista funktiota, ole tarkkana ja
huomioi seuraava rajoitus, joka selviää esimerkkien avulla. Esimerkeissä käytämme
ylempänä määriteltyä tuplaa
-funktiota.
Tässä ensin ylempänä käsitelty yksinkertainen tapaus vertailukohdaksi. Halutaan lisätä lukuun kahdesti ykkönen, esimerkiksi tuottaa luvusta 1000 luku 1002:
Pidempi funktioliteraali | Lyhennetty literaali | Toimiiko? | ||
---|---|---|---|---|
kahdesti( x => x + 1 , 1000) |
kahdesti( _ + 1 , 1000) |
Molemmat toimivat. |
Entäpä seuraava esimerkki? Nyt haluttaisiin toistaa kahdesti tätä: lisää ykkönen ja tuplaa
sitten. Esimerkiksi luvusta 1000 saataisiin tulos 4006. Funktioliteraalissa siis kutsutaan
tuplaa
-funktiota:
Pidempi funktioliteraali | Lyhennetty literaali | Toimiiko? |
---|---|---|
kahdesti( x => tuplaa(x + 1) , 1000) |
kahdesti( tuplaa(_ + 1) , 1000) |
Pidempi toimii, lyhyempi ei. |
Tässä tapauksessa lyhennetty versio ei tarkoita sitä, mitä haluttiin, vaan alaviiva
"lavennetaan sisimpien sulkujen sisällä". Literaali tuplaa(_ + 1)
tarkoittaakin
tuplaa(x => x + 1)
, joka ei ole toimiva lauseke. Käytä vastaavissa tilanteissa
pidempää merkintätapaa.
Alla on vielä kolmas esimerkki, jossa tehdään kahdesti seuraava: tuplaa ja lisää sitten ykkönen. Esimerkiksi luvusta 1000 saadaan siis tulos 4003.
Pidempi funktioliteraali | Lyhennetty literaali | Toimiiko? |
---|---|---|
kahdesti( x => tuplaa(x) + 1 , 1000) |
kahdesti( tuplaa(_) + 1 , 1000) |
Molemmat toimivat. |
Kun funktioliteraalin sisältämälle funktiokutsulle (tässä: tuplaa
-funktiolle) kirjataan
parametrilausekkeeksi pelkkä alaviiva, niin tuo alaviiva "lavennetaan sulkujen ulkopuolelle".
Äskeinen lyhennyskin siis toimii.
Jos säännöstö tuntuu epäselvältä, niin voit mainiosti kirjoittaa myös pitkällä literaalilla (kuten aina voit!).
Tehtävä: nimettömiä parametreja ja niiden rajoituksia
Bonusaihe: funktion palauttaminen funktiosta
Funktion toisen palautusarvona
Luku 6.1 mainitsi, että korkeamman asteen funktioiksi sanotaan "funktioita, jotka käsittelevät funktioita" eli sellaisia funktioita, jotka joko ottavat parametreiksi funktioita tai palauttavat funktioita. Kaikki materiaalissa esiintyneet korkeamman asteen funktiot ovat kuitenkin nimenomaan ottaneet parametreiksi funktioita.
Kurssin tehtävissä emme kirjoita funktioita, jotka palauttavat funktioita, mutta tässä kuitenkin yksinkertainen esimerkki siitä:
def tuotaSummaajafunktio(lisays: Int): Int => Int = { def summaaja(n: Int) = n + lisays summaaja }tuotaSummaajafunktio: (lisays: Int)Int => Int val kympinLisaaja = tuotaSummaajafunktio(10)kympinLisaaja: Int => Int = $$Lambda$5328/648803586@1347ef1b kympinLisaaja(5)res15: Int = 15 kympinLisaaja(6)res16: Int = 16 tuotaSummaajafunktio(100)(5)res17: Int = 105
Palautettavan funktion voi määritellä literaalillakin:
def tuotaSummaajafunktio(lisays: Int): Int => Int = _ + lisaystuotaSummaajafunktio: (lisays: Int)Int => Int
Yhteenvetoa
- Kuten esimerkiksi merkkijonoja ja lukujakin, myös funktioita voi kuvata literaaleina eli "ohjelmakoodiin kirjaimellisesti kirjoitettuina arvoina". Funktioliteraaleilla määritellään nimettömiä funktioita.
- Harkiten käytettyinä nimettömät funktiot voivat kätevöittää koodin kirjoittamista ja parantaa ohjelman luettavuutta.
- Nimettömät funktiot ovat hyödyllisiä erityisesti silloin, kun funktion määrittelyyn on tarvetta viitata vain yhdessä kohdassa kyseistä ohjelmakoodia. Tyypillinen käyttötapaus on "kertakäyttöisen" funktion välittäminen parametriksi korkeamman asteen funktiolle.
- Scalassa on kaksi tapaa kirjoittaa funktioliteraaleja. Pidemmässä,
jossa käytetään nuolta
=>
, parametrit nimetään. Lyhyemmässä, jossa käytetään alaviivaa_
, paitsi itse funktio myös parametrit ovat nimettömiä.- Nuolellinen merkintätapa toimii aina. Alaviivallinen toimii monissa muttei kaikissa yhteyksissä.
- Voit itse käyttää aina nuolta, mutta molempia merkintätapoja on tarpeen osata lukea.
- Lukuun liittyviä termejä sanastosivulla: funktioliteraali, nimetön funktio, nimetön parametri.
Palaute
Huomaathan, että tämä on henkilökohtainen osio! Vaikka olisit tehnyt lukuun liittyvät tehtävät parin kanssa, täytä palautelomake itse.
Tekijät
Tämän oppimateriaalin kehitystyössä on käytetty apuna tuhansilta opiskelijoilta kerättyä palautetta. Kiitos!
Kierrokset 1–13 ja niihin liittyvät tehtävät ja viikkokoosteet on laatinut Juha Sorva.
Kierrokset 14–20 on laatinut Otto Seppälä. Ne eivät ole julki syksyllä, mutta julkaistaan ennen kuin määräajat lähestyvät.
Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.
Tehtävien automaattisen arvioinnin ovat toteuttaneet: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä ja Aleksi Vartiainen.
Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.
Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista ovat suunnitelleet Juha Sorva ja Teemu Sirkiä. Niiden teknisen toteutuksen ovat tehneet Teemu Sirkiä ja Riku Autio käyttäen Teemun toteuttamia Jsvee- ja Kelmu-työkaluja.
Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset on laatinut Juha Sorva.
O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.
Opetustapa, jossa käytämme O1Libraryn työkaluja (kuten Pic
) yksinkertaiseen graafiseen
ohjelmointiin on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi
oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.
Oppimisalusta A+ on luotu Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Pääkehittäjänä toimii tällä hetkellä Jaakko Kantojärvi, jonka lisäksi järjestelmää kehittävät useat tietotekniikan ja informaatioverkostojen opiskelijat.
Kurssin tämänhetkinen henkilökunta on kerrottu luvussa 1.1.
Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.
kahdesti
-funktion ensimmäinen parametri on määritelty funktioliteraalilla (function literal). Funktioliteraali kuvaa nimettömän funktion (anonymous function). Tämän literaalin voi lukea: "eräs nimetön funktio, joka ottaaluku
-nimisen parametrin ja palauttaa arvonluku + 1
".kahdesti
-metodille välitetään parametriksi viittaus tällaiseen nimettömään funktioon.