Tämä kurssi on jo päättynyt.

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: HigherOrderAuctionHouse1-projektia hipaistaan.

../_images/person09.png

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, joten korotus-muuttujan nimi ei selkiytä ohjelmaa;
  • korotus-muuttujaa oletettavasti tarvittaisiin vain yhdessä käskyssä; arvo 1 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
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 ottaa luku-nimisen parametrin ja palauttaa arvon luku + 1". kahdesti-metodille välitetään parametriksi viittaus tällaiseen nimettömään funktioon.
Funktioliteraalin merkkinä on oikealle osoittava nuoli. Sen vasemmalla puolella mainitaan parametrit (joita on tässä vain yksi) ja oikealla puolella on funktion ohjelmakoodi (tässä vain yksi summalauseke).

Tässä taas kahdesti tuplaaminen nimetöntä funktiota käyttäen:

kahdesti(n => 2 * n, 1000)res3: Int = 4000
Funktioliteraali määrittelee nimettömän funktion, joka palauttaa kaksi kertaa parametriarvoaan suuremman kokonaisluvun.
Silloin kun se ei aiheuta suurempaa epäselvyyttä, funktioliteraalien parametrimuuttujat nimetään usein lyhyesti, jotta literaali ei kasva kovin isokokoiseksi. Tässä voisi nimen 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>
REPL vahvistaa, että funktioliteraalin arvo on tyyppiä Int => Int.
Parametrityypin 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 seuraavalle parametreja. Toisin kuin tarkoitimme, Scala-kääntäjä tulkitsi ilmaisun yritykseksi kutsua seuraavaa, 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

Tarkoitus on täydentää tämä korkeamman asteen metodikutsu:

myPic.transformColors( ??? )

Tässä on eräs funktioliteraali, joka sopii kysymysmerkkien paikalle:

pixel => pixel.lighter

Voiko tuon funktioliteraalin kirjoittaa uusiksi käyttäen nimellisen parametrin paikalla alaviivalla merkittyä nimetöntä parametria?

Tarkoitus on täydentää tämä korkeamman asteen metodikutsu:

Pic.generate(256, 256, ??? )

Tässä on eräs funktioliteraali, joka sopii kysymysmerkkien paikalle:

(x, y) => Color(x, x, y)

Voiko tämän literaalin kirjoittaa uusiksi vastaavasti kuin edellisessä kohdassa?

Tarkoitus on täydentää tämä korkeamman asteen metodikutsu:

Vector.tabulate(5, 10)( ??? )

Tässä on eräs funktioliteraali, joka sopii kysymysmerkkien paikalle:

(rivi, sarake) => sarake - rivi * 2

Voiko tämän literaalin kirjoittaa uusiksi vastaavasti kuin edellisissä kohdissa?

Tarkoitus on täydentää tämä korkeamman asteen metodikutsu:

Vector.tabulate(10)( ??? )

Tässä on eräs funktioliteraali, joka sopii kysymysmerkkien paikalle (kunhan Random on importattu):

indeksi => Random.nextInt(10) + indeksi

Voiko tuon literaalin kirjoittaa uusiksi vastaavasti kuin edellisissä kohdissa?

ylaraja => Random.nextInt(1 + ylaraja)

Voiko tuon literaalin kirjoittaa uusiksi vastaavasti?

Entäpä tämä variaatio?

luku => 1 + Random.nextInt(luku)

Tarkoitus on täydentää tämä korkeamman asteen metodikutsu:

turnElementsIntoResult(vectorOfInts, 0, ??? )

Tässä on eräs funktioliteraali, joka sopii kysymysmerkkien paikalle:

(count, nextElem) => count + (if (nextElem < 0) 1 else 0)

Voiko tämän literaalin kirjoittaa uusiksi vastaavasti kuin edellisissä kohdissa?

Tarkoitus on täydentää nämä korkeamman asteen metodikutsut:

val kertotauluNollastaAlkaen = Vector.tabulate(10, 10)( (rivi, sarake) => rivi * sarake )
val kertotauluYkkosestaAlkaen = Vector.tabulate(10, 10)( (rivi, sarake) => (rivi + 1) * (sarake + 1) )

Ensimmäisen noista riveistä voi kirjoittaa näinkin:

val kertotauluNollastaAlkaen = Vector.tabulate(10, 10)( _ * _ )

Voiko toisen kirjoittaa näin?

val kertotauluYkkosestaAlkaen = Vector.tabulate(10, 10)( (_ + 1) * (_ + 1) )

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.

a drop of ink
Palautusta lähetetään...