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

Luku 5.5: Funktioita parametreina

Tästä sivusta:

Pääkysymyksiä: Miten funktiot voivat käsitellä funktioita? Miten välitän funktiolle parametriksi funktion? Miksi on hienoa, että voin tehdä niin?

Mitä käsitellään? Pääaiheena ovat korkeamman asteen funktiot, erityisesti funktioparametrit. Lisäksi käsitellään usean parametriluettelon kirjaamista Scala-funktiolle, uusia menetelmiä kokoelmien luomiseen sekä sisäkkäisiä eli "moniulotteisia" kokoelmia. Paljon kuvankäsittelyesimerkkejä.

Mitä tehdään? Luetaan ja ohjelmoidaan. Useita pieniä tehtäviä.

Suuntaa antava työläysarvio:? Kolme ja puoli tuntia.

Pistearvo: A50 + B25 + C15.

Oheisprojektit: HigherOrder (uusi). AuctionHouse1-projektikin käy ohimennen esimerkkinä.

../_images/person03.png

Johdanto

Tähän mennessä on tullut selväksi, että ohjelmiin liittyy:

  1. dataa — esimerkiksi lukuja, tekstiä ja muita olioita — jota voi tallentaa muistiin ja johon voi kohdistaa:
  2. toimenpiteitä — funktioita — jotka tekevät asioita datalla ja jotka voivat olla kytköksissä tietynlaiseen dataan (kuten metodit olio-ohjelmoinnissa ovat).

Kahtiajako ei kuitenkaan ole niin jyrkkä kuin millaisena se on toistaiseksi näyttäytynyt. Myös funktiot ovat näet dataa, ja niitä voi esimerkiksi tallentaa muuttujiin, välittää parametreiksi toisille funktioille ja palauttaa toisen funktion palautusarvoina.

Ennen kuin käsittelemme sitä, mitä hyötyä tällaisesta voi olla, katsotaan konkreettinen esimerkki.

Funktion välittäminen parametrina

Lyhyen tähtäimen suunnitelma:

  1. Määritellään kaksi pientä funktiota — seuraava ja tuplaa — jotka kuvaavat sellaisia toimenpiteitä, jotka voi kohdistaa kokonaislukuun ja jotka tuottavat kokonaislukutuloksen.
  2. Määritellään funktio nimeltä kahdesti, jolla voi suorittaa minkä tahansa tuollaisen kokonaislukufunktion kahteen kertaan. Tieto siitä, mikä funktio suoritetaan kahteen kertaan, välitetään kahdesti-funktiolle parametriksi.

Aloitetaan kohdasta yksi ja määritellään aluksi pari ihan tavallista, irrallista funktiota. Ensin tämä:

def seuraava(luku: Int) = luku + 1seuraava: (luku: Int)Int
seuraava(100)res0: Int = 101
seuraava-funktio yksinkertaisesti palauttaa parametriarvoaan yhtä isomman kokonaisluvun.
REPL ilmoittaa näin, että seuraava-nimi viittaa funktioon, joka ottaa parametriksi Int arvon...
... ja myös palauttaa Int-arvon. Voidaan sanoa, että seuraava-funktio on tyyppiä Int => Int, missä nuolen vasemmalla puolella on parametrin tyyppi ja oikealla palautusarvon tyyppi.

tuplaa-funktio on sekin tyyppiä Int => Int:

def tuplaa(tuplattava: Int) = 2 * tuplattavatuplaa: (tuplattava: Int)Int
tuplaa(100)res1: Int = 200

kahdesti-funktio

kahdesti-funktion tehtävänä on suorittaa parametriksi annettu toimenpide kahteen kertaan. Haluttaisiin, että sitä voisi käyttää näin:

kahdesti(seuraava, 1000)res2: Int = 1002

Huomaa: kahdesti-funktion ensimmäiseksi parametriksi annetaan funktio! Näin kerrotaan, mitä funktiota pitäisi soveltaa kaksi kertaa. Jälkimmäinen parametriarvo kertoo soveltamisen kohteen, joka on ihan tavallinen kokonaisluku.

Kun seuraava-funktiota sovellettiin kahdesti, saatiin kahdella suurempi luku. Kahdesti tuplaamalla taas saadaan nelinkertainen luku:

kahdesti(tuplaa, 1000)res3: Int = 4000

Määritellään kahdesti-funktio näin:

def kahdesti(toiminto: Int => Int, kohde: Int) = toiminto(toiminto(kohde))kahdesti: (toiminto: Int => Int, kohde: Int)Int
Ensimmäisen parametrin tietotyypiksi on merkitty Int => Int. Tämä tarkoittaa, että parametriksi voi antaa (ja pitää antaa) minkä tahansa sellaisen funktion, joka ottaa parametriksi yhden kokonaisluvun ja joka myös palauttaa kokonaisluvun. Siis juuri sellaisen funktion, jollaisia seuraava ja tuplaa ovat.
kahdesti-funktio toimii siten, että kutsutaan ensimmäiseksi parametriksi saatua funktiota toiseksi parametriksi välitetylle kohdeluvulle ensin kerran ja sitten saadulle palautusarvolle uudestaan.
Huomaa! Missään ei ole määritelty def-sanalla mitään toiminto-nimistä funktiota. toiminto on ihan tavallinen parametrimuuttuja. Uutta siinä on vain se, että se sisältää viittauksen funktioon. Käsky toiminto(...) siis toimii sen funktion kutsuna, joka kyseisellä kahdesti-funktion kutsukerralla on annettu ensimmäiseksi parametriksi.
REPLin tulosteisiin täytyy hieman totutella. Esimerkiksi tämä tuloste kertoo, että nimi kahdesti viittaa funktioon, joka...
1) ottaa ensimmäiseksi parametrikseen funktion, joka ottaa yhden kokonaislukuparametrin ja palauttaa kokonaisluvun.
  1. ottaa toiseksi parametrikseen kokonaisluvun.
  1. ja palauttaa kokonaisluvun.

Väliajatus

Vertaile keskenään:

  • Funktio voidaan välittää parametriksi toiselle funktiolle.
  • Tietokoneohjelma voi ottaa syötteenä toisen tietokoneohjelman. Esimerkiksi kääntäjä muuntaa annetun ohjelman toiseen muotoon, ja virtuaalikone ajaa annetun ohjelman.
  • Matematiikassa derivaatan laskeminen on toimenpide, jonka "syötteenä" on derivoitava funktio. (Tuloksena saadaan myös funktio.)

Abstraktioista ja korkeamman asteen funktioista

Mikä tahansa parametrillinen funktio on abstraktio — yleistys — niistä kaikista eri konkreettisista tilanteista, joita saadaan aikaan kutsumalla funktiota erilaisilla parametriarvoilla. Esimerkiksi funktio tuplaa on abstraktio kaikista eri tapauksista, joissa jokin kokonaisluku kerrotaan kahdella.

Sellainen funktio, joka ottaa parametrikseen funktion, on abstraktio abstraktioista. Esimerkiksi kahdesti on abstraktio kaikista sellaisista tilanteista, joissa suoritetaan kahdesti jokin sellainen yhdelle kokonaisluvulle määritelty toimenpide, jonka tuloksena on myös kokonaisluku (kuten tuplaaminen tai yhdellä kasvatus).

Funktioita, jotka vastaanottavat funktioita parametreinaan ja/tai palauttavat funktioita palautusarvoina, sanotaan usein korkeamman asteen funktioiksi (higher-order function). Muita (ennestään tutunlaisia) funktioita voi vertailun vuoksi sanoa ensimmäisen asteen funktioiksi (first-order function).

Jotkin ohjelmointikielet eivät tue kuin ensimmäisen asteen funktioita. Kuitenkin monissa kielissä voi myös laatia korkeamman asteen funktioita. Scalassakin tämä on mahdollista, kuten jo näitkin.

Lisäsanastoa

Voit kuulla puhuttavan myös "funktioista ensiluokan kansalaisina" (functions as first-class citizens, tai first-class functions). Tällä tarkoitetaan juuri sitä, että funktioita voi välittää parametreina ja palautusarvoina sekä tallentaa muuttujiin aivan yhtä hyvin kuin vaikkapa lukujakin. Ensiluokan kansalaisuus siis vaatii, että käytettävissä ei ole ainoastaan ensimmäisen asteen funktioita.

Käyttötilanteita

kahdesti-esimerkkifunktio saattaa tuntua kikkailulta, jolla ei ole käytännön merkitystä. Kuitenkin osoittautuu, että funktioiden välittäminen parametreiksi on erittäin usein käytännöllistä. Esimerkkejä:

  1. Tilanne: Halutaan pystyä muokkaamaan kuvan pikseleitä monilla erilaisilla tavoilla, joista vain osa on tiedossa etukäteen. Värikuvan pikseleitä saatetaan esimerkiksi haluta vaihtaa harmaasävyisiin tai kuvaa voidaan pehmentää tai kirkastaa tai mitä nyt keksitäänkään. Tarvitaan tapa sanoa: "suorita jokaiselle kuvan pikselille tällä kertaa tämä toimenpide".
    • Ratkaisu: Kutsutaan metodia, jolle ilmoitetaan parametrilla se funktio, joka halutaan suoritettavan jokaiselle pikselille vuoron perään.
  2. Tilanne: On käytössä käyttöliittymän nappulaa kuvaava olio. Halutaan ilmoittaa, että "kun tätä nappulaa painetaan, niin suoritetaan tietty ohjelmakoodi".
    • Ratkaisu: Kutsutaan metodia, jolle ilmoitetaan parametrilla se funktio, joka halutaan suoritettavan (vasta) silloin, kun nappulaa painetaan.
  3. Tilanne: Halutaan laatia metodi, jolla voi järjestää olioita — vaikkapa henkilöitä — sisältävä luettelo. Osana järjestämisalgoritmia on osattava verrata kahta oliota keskenään, jotta tiedetään, mikä niiden keskinäisen järjestyksen tulisi olla. Halutaan, että on erilaisia järjestämiskriteereitä: voidaan järjestää henkilöt vaikkapa nimen tai syntymävuoden mukaan. Halutaan siis voida sanoa: "Järjestä oliot käyttäen tällä kertaa tätä tapaa olioiden vertailemiseen."
    • Ratkaisu: Välitetään järjestämismetodille sellainen funktio, joka pyytää henkilöolioilta kriteeriä vastaavan tiedon (nimen, vuoden, tms.) ja vertaa olioita keskenään.
  4. Tilanne: On käytössä kokoelma, jonka alkioina on jonkinlaisia olioita, vaikkapa mittaustuloksia. Etukäteen ei ole tiedossa ainakaan kaikkia niitä toimenpiteitä, jotka halutaan mittaustulokselle suorittaa.
    • Ratkaisu: Laaditaan tuloskokoelmaa kuvaavalle oliolle metodi, jolla voi käsitellä sen sisältöä joustavasti: metodille voi antaa parametriksi funktion, joka kuvaa sitä toimenpidettä, joka suoritetaan kullekin yksittäiselle mittaustulokselle.

Kurssin mittaan näet paitsi yllä mainittuja tilanteita muistuttavat tapaukset myös paljon muita esimerkkejä korkeamman asteen funktioista. Korkeamman asteen funktiot tulevat olemaan runsaassa käytössä koko loppukurssin läpeensä.

Esimerkki: merkkijonoja

Ylemmässä esimerkissä käytimme parametrina funktiota, joka ottaa yhden Int-parametrin ja palauttaa yhden Int-arvon. Myös muunlaisia funktioita voi toki käyttää parametreina. Tarkastellaan esimerkkinä vaikkapa merkkijonojen vertailua.

Merkkijonoja voi vertailla keskenään erilaisin perustein. Esimerkiksi seuraavat kolme funktiota vertaavat merkkijonoja pituuden mukaan, merkkijonon sisältämän lukuarvon mukaan (olettaen, että merkkijono sisältää vain numeromerkkejä) ja Unicode-aakkosjärjestyksen mukaan:

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)

Kokeillaan nyt tehdä funktio onkoJarjestyksessa, joka selvittää, ovatko annetut kolme merkkijonoa järjestyksessä. Se, mitä "järjestyksellä" tarkoitetaan, jätetään onkoJarjestyksessa-metodin kutsujan määriteltäväksi: vertailuperusteen voi ilmoittaa antamalla parametriksi funktion, joka huolehtii kahden merkkijonon vertailusta jotakin kriteeriä käyttäen.

Halutaan siis, että onkoJarjestyksessa-funktio toimii tähän tapaan:

onkoJarjestyksessa("Java", "Scala", "Haskell", vertaaPituuksia)res4: Boolean = true
onkoJarjestyksessa("Haskell", "Java", "Scala", vertaaPituuksia)res5: Boolean = false
onkoJarjestyksessa("Java", "Scala", "Haskell", vertaaMerkkeja)res6: Boolean = false
onkoJarjestyksessa("Haskell", "Java", "Scala", vertaaMerkkeja)res7: Boolean = true
onkoJarjestyksessa("200", "123", "1000", vertaaIntArvoja)res8: Boolean = false
onkoJarjestyksessa("200", "123", "1000", vertaaPituuksia)res9: Boolean = true

Funktion voi toteuttaa vaikkapa näin:

def onkoJarjestyksessa(eka: String, toka: String, kolmas: String, vertaa: (String, String) => Int) =
  vertaa(eka, toka) <= 0 && vertaa(toka, kolmas) <= 0
vertaa-parametrin tyyppinä on "funktio, joka ottaa kaksi merkkijonoparametria ja palauttaa kokonaisluvun".
Kaarisulut parametriluettelon ympärillä ovat tässä pakolliset, kun parametreja on yli yksi, jotta parametriluettelo erottuu ympäristöstään. (Sulut olisi saanut kirjoittaa myös kahdesti-funktiossa näin: (Int) => Int.)
onkoJarjestyksessa-metodi käyttää vertaa-funktiota kahdesti tarkastaakseen merkkijonoparien sisäisen järjestyksen.

Esimerkki: kokoelman alkioiden käsittely

Täydennetään hieman luvun 5.3 AuctionHouse-luokkaa. Otetaan pari lisätavoitetta. Halutaan, että:

  1. AuctionHouse-olioilla on metodi, jolla voi pyytää luettelon kaikista sellaisista myyntiin laitetuista esineistä, jotka ovat parhaillaan ostettavissa eli joita ei ole vielä ostettu ja joiden myyntiaika ei ole mennyt umpeen.
  2. AuctionHouse-olioilla on metodi, jolla voi pyytää luettelon kaikista sellaisista myyntiin laitetuista esineistä, joiden kuvauksessa esiintyy tietty tekstinpätkä.
  3. Vastaavasti voidaan muodostaa luetteloja myös muilla kriteereillä mahdollisimman joustavasti.

Yksi vaihtoehto voisi olla se, että laadimme AuctionHouse-luokkaan erilliset metodit kuhunkin eri tarpeeseen — esim. findAllOpenItems ja findAllMatchingKeyword — ja yrittäisimme ennakoida kaikki yleiset hakutarpeet. Joustavampi ratkaisu saadaan abstrahoimalla: laaditaan yleiskäyttöinen metodi findAll, jolle välitetään hakukriteeri parametriksi ja joka palauttaa luettelon kaikista tämän kriteerin täyttävistä esineistä. Hakukriteeri voidaan kuvata funktiona:

class AuctionHouse {

  private val items = Buffer[EnglishAuction]()

  // ... muita metodeita ...

  def findAll(checkCriterion: EnglishAuction => Boolean) = {
    val found = Buffer[EnglishAuction]()
    for (currentItem <- this.items) {
      if (checkCriterion(currentItem)) {
        found += currentItem
      }
    }
    found.toVector
  }

}
Metodille findAll tulee kutsuessa antaa parametriksi funktio, joka 1) ottaa parametrikseen esineen, 2) selvittää, täyttääkö tuo esine jonkin kriteerin, ja 3) palauttaa kriteerin täyttymisestä kertovan totuusarvon.
Algoritmin perusajatus on luvusta 5.3 tuttu: aloitetaan tyhjällä tulospuskurilla, käydään silmukassa kaikki esineet läpi ja selvitetään kultakin, täyttyykö hakukriteeri. Tulospuskuriin lisätään kaikki, joille kriteeri täyttyy.
Parametrifunktiota käytetään if-käskyn sisällä tarkastettaessa, täyttyykö hakukriteeri.

Nyt luokkaa voi käyttää vaikkapa 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

}

Luvussa 6.2 selviää, että Scalan kokoelmaluokille (kuten Vector) on valmiiksi määriltelty käteviä korkeamman asteen metodeita, joilla voi tehdä samantapaisia asioita kuin tässä luodulla findAll-metodilla.

Esimerkki: kuvan värien muunnos

Entäpä yllä jo esiin tullut ajatus kuvan muokkaamisesta kohdistamalla toimenpide sen kuhunkin pikseliin? Vaikkapa tämä toimenpide:

def swapGreenAndBlue(original: Color) = Color(original.red, original.blue, original.green)
Kertaus luvusta 5.2: uuden värisävyn voi luoda kolmesta RGB-komponenteista: ensin punainen, sitten vihreä, sitten sininen.
Tämä funktio ottaa (pikselin) värin ja palauttaa toisen värin, jossa on funktion nimen mukaisesti...
... vaihdettu sinisen ja vihreän arvot keskenään. Uudessa sävyssä on siis sen verran sinistä kuin alkuperäisessä oli vihreää ja toisin päin.

Pic-luokasta löytyy korkeamman asteen metodi transformColors, jolla tuo värimuunnos on helppo kohdistaa kuhunkin pikseliin:

val originalPic = Pic("defense.png")originalPic: Pic = defense.png
val manipulatedPic = originalPic.transformColors(swapGreenAndBlue)manipulatedPic: Pic = defense.png (transformed)
originalPic.leftOf(manipulatedPic).show()
transformPic-metodille annetaan parametriksi jokin Color => Color -tyyppinen funktio, tässä swapGreenAndBlue. Se soveltaa tuota funktiota kuhunkin pikseliin ja palauttaa näin muodostamansa uuden kuvan.
../_images/swapGreenAndBlue.png

Esimerkkikoodin tuotos.

Pikkutehtäviä: värisuotimet

Realistinen harmaasävysuodin

Toimintoa, joka suoritetaan kuvan (tai kuvan osan) pikseleille, sanotaan suotimeksi (filter). Esimerkiksi äskeinen koodi toteuttaa vihreän ja sinisen vaihtavan suotimen.

Toinen esimerkki on värikuvan harmaasävyiseksi muuttava suodin. Eräs sellainen on toteutettu projektin HigherOrder tiedostoon Task1.scala.

Avaa tiedosto ja lue sieltä löytyvä koodi, joka muistuttaa äskeistä suodinesimerkkiämme. Samasta tiedostosta löytyy myös lyhyt tehtävänanto; toimi sen mukaisesti.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Piilotettuja kuvia

../_images/hidden1.png

Mitä oheinen kuva esittää? Entä hieman alempana oleva?

Vaikka se ei päälle päin näy, niin näiden kahden kuvan pikseleihin on piilotettu ihmisen katsottavaksi sopivia kuvia. Kuvat on tarkoituksellisesti "sotkettu" muokkaamalla pikselien värisävyjä niin, ettei kuva näytä ihmiselle juuri miltään. Kuitenkin riittävä informaatio kuvien "restauroimiseksi" katselukelpoisiksi on edelleen näissä kuvissa tallella.

Pääset nyt ratkaisemaan kuva-arvoitukset Scala-koodia kirjoittamalla.

Laadi ensin suodin, joka muokkaa pisteiden väriarvoja niin, että ylempään kuvatiedostoon piilotettu kuva tulee esiin. Tarkemmat ohjeet löytyvät tiedostosta Task2.scala.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Toinen arvoitus

../_images/hidden2.png

Ratkaise toinenkin kuva-arvoitus. Ratkaisun avaimet löytyvät tiedostosta Task3.scala.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Lisää aiheesta

Jos kuvienpiilottamisteema kiinnostaa, niin jatkolukemiseksi sopii esimerkiksi Wikipedian Steganography-artikkeli.

Mielenkiintoista ja vähän pelottavaa luettavaa tarjoaa vuoden 2017 tutkimus, joka näyttää, että pahantahtoinen tekijä voi tuottaa ääntä, joka kuulostaa ihmisestä viattomalta mutta jossa on puhetta tunnistavaa ohjelmaa komentava piiloviesti. Ks. Audio Adversarial Examples.

Kuvien luomista korkeamman asteen metodilla

../_images/blue_gradient.png

Äskeisissä tehtävissä muokattiin olemassa olevia kuvia soveltamalla kuhunkin pikseliin tiettyä funktiota. Aivan vastaavalla tavalla voidaan myös luoda kokonaan uusia kuvia pikseli kerrallaan, ja siihenkin on tarjolla valmis työkalu.

val size = 256size: Int = 256
def blueGradient(x: Int, y: Int) = Color(0, 0, x.toDouble / (size - 1) * Color.Max)blueGradient: (x: Int, y: Int)Color
val pic1 = Pic.generate(size, size, blueGradient)pic1: Pic = generated pic
pic1.show()
Määritellään ensin funktio, joka saa parametreikseen x- ja y-koordinaatit ja palauttaa niitä vastaavan värin.
Funktiomme palauttaa värin, jossa ei ole punaista eikä vihreää, mutta...
... sinistä on sitä enemmän, mitä suurempi x-koordinaatti on. (Vakio Color.Max on kunkin RGB-komponentin erilaisten arvojen määrä, tässä käytännössä 256, koska arvot ovat välillä 0–255.)
Käskyllä Pic.generate (joka on Pic-kumppaniolion metodikutsu; luku 5.1) voi tuottaa uuden kuvan. Parametreiksi annetaan leveys, korkeus ja funktio, jota kutsutaan joka pikselille sen sävyn määrittämiseksi.
show-käsky näyttää oheisen kuvan.
../_images/generated_art.png

Tässä toinen esimerkki, jossa kuvapisteen väri määrittyy hieman mutkikkaammin:

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).show()

Kokeile itsekin. Avaa Task4.scala ja tee sieltä löytyvä minitehtävä.


A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Kokeile ihmeessä generoida muitakin kuvia annettujen esimerkkien lisäksi.

Sivupolku: Useita parametriluetteloja samassa funktiossa

Ennen kuin jatkat tämän luvun viimeisiin esimerkkeihin ja tehtäviin, on syytä tuntea eräs Scala-kielen erikoispiirre.

Tähän asti olemme luetelleet kaikki parametrit pilkuilla erotettuina yksien kaarisulkeiden sisään. Kyseisillä funktioilla on siis ollut yksi parametriluettelo (parameter list). Hyvin monet Scala-funktiot ovat juuri tällaisia.

Scala-funktiolle voi määritellä myös useita parametriluetteloita:

def kokeilu(eka: Int, toka: Int)(kolmas: Int, neljas: Int) = eka * toka + kolmas * neljaskokeilu: (eka: Int, toka: Int)(kolmas: Int, neljas: Int)Int
Funktion kokeilu määrittelyssä on kahdet parametriluetteloa merkitsevät kaarisulkeet peräkkäin. Funktion neljä parametria on ryhmitelty kahteen osaan.

Kun funktio on määritelty kuten yllä, eivät yhdet sulkeet kaikkien neljän parametriarvon ympärillä riitä, vaan on käytettävä kaksia sulkeita myös kutsuessa:

kokeilu(1, 2)(3, 4)res10: Int = 14
kokeilu(1, 2, 3, 4)<console>:9: error: too many arguments for method kokeilu: (eka: Int, toka: Int)(kolmas: Int, neljas: Int)Int

On olemassa tapauksia, joissa usean parametriluettelon käyttö on kätevää. Aihetta ei kuitenkaan tällä kurssilla juurikaan käsitellä, eikä sinun tarvitse kurssin puitteissa lainkaan laatia sellaisia funktioita, joilla on useita parametriluetteloja. Sen sijaan tälläkin kurssilla on tarvetta kutsua eräitä Scalan valmiita funktioita, joille pitää antaa parametreja kahdessa erillisessä luettelossa. Yksi tällainen tilanne on seuraavassa esimerkissä.

Vaativa lisätehtävä

Jos haluat tutustua tarkemmin tähän aiheeseen, voit etsiä internetistä tietoa hakusanalla currying. Varoitus: lähteet eivät välttämättä ole helposti ymmärrettävissä pelkillä tämän kurssin tarjoamilla pohjatiedoilla (koska käyttävät joko toisia ohjelmointikieliä tai sellaisia Scalan piirteitä, joita emme käsittele).

Voit myös halutessasi selvittää, millaisissa tilanteissa usean parametriluettelon käyttö tehostaa Scalan tyyppipäättelyä.

Kokoelmien luomista korkeamman asteen metodilla

tabulate-metodi

Aivan kuin pystyimme luomaan kuvan pikseli kerrallaan metodilla Pic.generate, pystymme myös luomaan kokoelman alkio kerrallaan. Siihen sopii tabulate-niminen metodi.

Palautetaan ensin mieleen luvun alun kokeilufunktiot:

def seuraava(luku: Int) = luku + 1seuraava: (luku: Int)Int
def tuplaa(tuplattava: Int) = 2 * tuplattavatuplaa: (tuplattava: Int)Int

Tehdään kokeeksi vaikkapa kokonaislukuvektori, jossa kukin alkio on tuplasti indeksin suuruinen:

Vector.tabulate(10)(tuplaa)res11: Vector[Int] = Vector(0, 2, 4, 6, 8, 10, 12, 14, 16, 18)
tabulate-metodille annetaan kaksi parametriluetteloa. Ensimmäisessä on haluttu alkioiden määrä eli luotavan vektorin koko ja toisessa funktio, jota kutsutaan kullekin indeksille kyseisen alkion muodostamiseksi.
Alkiot muodostuvat, kun tabulate kutsuu parametrina saamaansa funktiota (tässä tuplaa) kullekin indeksille (tässä luvuille 0–9).

Tässä vastaava esimerkki seuraava-funktiolla:

Buffer.tabulate(5)(seuraava)res12: Buffer[Int] = ArrayBuffer(1, 2, 3, 4, 5)

Kuten näkyy, tabulate on määritelty myös puskureille.

Lisää tabulate-esimerkkejä

tabulatelle parametriksi välitettävälle funktiolle annetaan parametriksi kokoelman indeksejä, joten sen täytyy vastaanottaa Int-tyyppisiä arvoja. Sen ei kuitenkaan tarvitse palauttaa kokonaislukuja:

def parity(index: Int) = index % 2 == 0parity: (index: Int)Boolean
val parillisuudet = Vector.tabulate(5)(parity)parillisuudet: Vector[Boolean] = Vector(true, false, true, false, true)
println(parillisuudet.mkString("\t"))true    false   true    false   true
parity funktio tutkii, onko annettu kokonaisluku parillinen, ja kertoo vastauksen Boolean-arvona.
Niinpä sitä käyttämällä saadaan aikaan Boolean-arvoja sisältävä vektori.
Tässä samalla pieni muistutus myös siitä, miten tulostetta voi kaunistella mkString-metodilla. Erottimena on tässä käytetty sarkain- eli tabulaattorimerkkiä, joka merkitään merkkijonoon \t (luku 4.5).

Vektorillinen kasvavahkoja satunnaislukuja, ole hyvä:

import scala.util.Randomimport scala.util.Random
def randomElement(upperLimit: Int) = Random.nextInt(upperLimit + 1)randomElement: (upperLimit: Int)Int
println(Vector.tabulate(30)(randomElement).mkString(","))0,0,1,3,4,3,2,1,4,1,0,11,2,13,12,7,6,8,16,4,7,16,14,4,10,24,19,26,15,24
Pystynet näkemään, miksi satunnaislukuvektorin loppupäässä on enemmän suuria lukuja kuin alkupäässä.

Pieni tabulate-tehtävä

Hieman samansuuntainen ohjelma löytyy tiedostosta Task5.scala. Täydennä se koodista löytyvän kommentin pyytämällä tavalla.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

"Moniulotteiset" kokoelmat

Kun nyt tabulate tuli puheeksi, niin miksi metodille on laitettu tuollainen "taulukointiin" viittaava nimi?

Luultavasti siksi, että sillä voi näppärästi luoda myös "kaksiulotteisia" kokoelmia. Ajatellaan vaikkapa tilannetta, jossa haluamme taulukkolaskentaohjelman tapaan käsitellä tällaista lukuja sisältävää rakennetta.

3 4 5
13 14 15

(Matematiikkaa tuntevat lukijat voivat myös nähdä tässä matriisin.)

Miten vastaavan rakenteen voisi luoda Scalalla? Tarvittaisiin "kaksiulotteinen vektori", jossa on rivi- ja sarakenumerot tai jotakin sinne päin.

Päätetään ensin, mitä lukuja haluaisimme esimerkkivektorissamme olevan. Tämä pikkufunktio kuvaa sen:

def alkioKohtaan(rivi: Int, sarake: Int) = rivi * 10 + sarake + 3alkioKohtaan: (rivi: Int, sarake: Int)Int
Jos rivit ja sarakkeet on numeroitu nollasta alkaen, niin tuo funktio tuottaa juuri yllä olevat esimerkkiluvut. Esimerkiksi sarakkeeseen 2 riville 1 halusimme 1*10+2+3 eli 15.

"Moniulotteisuus" on sisäkkäisyyttä

val kaksiulotteinen = Vector.tabulate(2, 3)(alkioKohtaan)kaksiulotteinen: Vector[Vector[Int]] = Vector(Vector(3, 4, 5), Vector(13, 14, 15))
tabulaten ensimmäisessä parametriluettelossa on tällä kertaa kaksi parametriarvoa: luotavan kokoelman korkeus ja leveys.
Toisessa on funktio, joka ottaa yhtä monta kokonaislukuparametria kuin vektorilla on ulottuvuuksia (tässä: kaksi) ja joka palauttaa vektoriin sopivan alkion (tässä: kokonaisluvun).
Tuloksena saadaan vektori "rivejä", joista kukin on vektori.
Tyyppikin sen kertoo: tämä on vektori, jonka alkiot ovat vektoreita, joiden alkiot ovat lukuja. Näennäinen kaksiulotteisuus on oikeastaan vain yksiulotteisten kokoelmien sisäkkäisyyttä.

Moniulotteisen kokoelman voi toki tehdä käsityönäkin, ilman tabulatea. Vaikka näin:

val kaksiSarakettaNeljaRivia = Vector(Vector(1, 2), Vector(3, 4), Vector(5, 6), Vector(7, 8))kaksiSarakettaNeljaRivia: Vector[Vector[Int]] = Vector(Vector(1, 2), Vector(3, 4), Vector(5, 6), Vector(7, 8))

Tarkastele seuraavaa ohjelmakoodia ja mieti, mitä se tulostaa:

def tuotaAlkio(rivi: Int, sarake: Int) = (rivi + sarake + 1) * 1.5
val vektori = Vector.tabulate(3, 2)(tuotaAlkio)
println(vektori(0)(0))
println(vektori.size)
println(vektori(0).size)
println(vektori(1).size)
val rivi = vektori(2)
println(rivi(1))

Kirjoita tuloste tähän:

Luo "kaksiulotteinen" puskuri (tai vektori) ja kokeile sille parametritonta metodia flatten. Mitä metodi tekee?

Usein kysyttyä: mistä tietää, miten päin rivit ja sarakkeet menee?

Vastaus: Vain siitä, miten kyseisessä ohjelmassa on määrätty niiden menevän. Voidaan tehdä sisäkkäinen kokoelma, jossa kukin alkio edustaa riviä ja sen sisällä ovat sarakkeittain varsinaiset arvot. Tai toisin päin: voidaan tehdä kokoelma, jossa kukin alkio edustaa saraketta ja sen sisällä ovat arvot riveittäin. Tämän luvun esimerkeissä käytettiin ensimmäistä tapaa, mutta mitään yleispätevää normia tähän ei ole.

Itse asiassa rivien esittämiseen ei välttämättä tarvita sisäkkäisiä kokoelmia lainkaan. Voitaisiin esimerkiksi sopia, että yksiulotteisessa kuusipaikkaisessa vektorissa indeksit 0–2 edustavat "kaksi kertaa kolme" -matriisin ensimmäistä riviä ja indeksit 3–5 toista riviä. Yleensä sisäkkäinen ratkaisu on kuitenkin kätevämpi.

Rivityksessä tehdyillä valinnoilla voi kyllä olla vaikutuksia suoritustehokkuuteen, mistä lisää jatkokursseilla.

Sisäkkäisten kokoelmien läpikäyntiä

Koska "moniulotteinen" kokoelma on vain yksiulotteisia kokoelmia sisäkkäin, ei sen käsittelyssä ole varsinaisesti mitään uutta. Tällaisen kokoelman voi käydä läpi samoilla konsteilla — esimerkiksi silmukalla — joita olet jo aiemmin käyttänyt.

Tämä esimerkki muodostaa ensin tabulatella kertotaulun:

def kertotauluun(rivi: Int, sarake: Int) = (rivi + 1) * (sarake + 1)kertotauluun: (rivi: Int, sarake: Int)Int
val vektorillinenRiveja = Vector.tabulate(10, 10)(kertotauluun)vektorillinenRiveja: Vector[Vector[Int]] = Vector(Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), Vector(2, 4, 6, ..., 20), , ..., Vector(10, 20, 30, ..., 100))

Jos nyt haluamme tulostaa kertotaulun riveittäin, voimme käydä läpi ulomman vektorin, ja käsitellä kunkin riviä kuvaavan sisemmän vektorin erikseen:

for (luvutRivilla <- vektorillinenRiveja) {
  println(luvutRivilla.mkString("\t"))
}1   2   3   4   5   6   7   8   9   10
2   4   6   8   10  12  14  16  18  20
3   6   9   12  15  18  21  24  27  30
4   8   12  16  20  24  28  32  36  40
5   10  15  20  25  30  35  40  45  50
6   12  18  24  30  36  42  48  54  60
7   14  21  28  35  42  49  56  63  70
8   16  24  32  40  48  56  64  72  80
9   18  27  36  45  54  63  72  81  90
10  20  30  40  50  60  70  80  90  100
Koska vektorillinenRiveja on Vector[Vector[Int]], jonka alkiotkin ovat vektoreita, niin...
... sieltä alkioita poimittaessa saadaan aina yksi sisempi vektori kerrallaan. Muuttujan luvutRivilla tyyppi on Vector[Int].
Sisempi vektori on ihan tavallinen lukuvektori. Tässä sen perusteella muotoillaan merkkijono ja tulostetaan se.

Tarkastele seuraavaa ohjelmakoodia. Mieti järjestelmällisesti, vaihe vaiheelta, miten tulosmuuttujan arvo muuttuu.

val lukuja = Vector(Vector(10, 20, 30, 0, 0), Vector(40, 0, 0, 0, 0))
val ehtoja = Vector(true, true, false, false, true)
var tulos = 0
for (i <- lukuja.indices) {
  for (j <- lukuja(i).indices) {
    if (ehtoja(j)) {
      tulos += lukuja(i)(j)
    }
  }
}
println(tulos)

Kirjoita tähän allekkain kaikki eri lukuarvot, jotka tulosmuuttuja saa tämän koodinpätkän suorittamisen aikana. (Älä toista mitään samaa lukua useaan kertaan.)

Kuvien yhdistäminen combine-metodilla

Tämä vapaaehtoinen välipala jatkaa ylempänä esiin nostettua kuvankäsittelyteemaa ja toimii lisäesimerkkinä korkeamman asteen metodien käytöstä.

Esimerkki: kuvien keskiarvo

../_images/combine1_input.png

Meillä on kaksi kuvaa, jotka haluamme yhdistää:

val pic1 = Pic("lostgarden/tree-tall.png")pic1: Pic = lostgarden/tree-tall.png
val pic2 = Pic("lostgarden/girl-horn.png")pic2: Pic = lostgarden/girl-horn.png

Yksi tapa yhdistää kuvat on laskea niiden pikselien väriarvoista keskiarvot kussakin koordinaateissa. Jokaiselle koordinaattiparille siis tehdään tällainen operaatio:

def naiveAverage(color1: Color, color2: Color) =
  Color((color1.red   + color2.red)   / 2,
        (color1.green + color2.green) / 2,
        (color1.blue  + color2.blue)  / 2)
../_images/combine1_output.png

Lisäksi tarvitsemme jonkin tavan kohdistaa tuo toimenpide kuhunkin koordinaattipariin kohdekuvissa. Siihen sopii Pic-luokan metodi combine, joka yhdistää kaksi kuvaa parametrina saamansa funktion määräämällä tavalla:

val combinedPic = pic1.combine(pic2, naiveAverage)combinedPic: Pic = combined pic
combinedPic.show()

Voit itsekin ajaa tämän ohjelman, joka löytyy tiedostosta Example7.scala. Kokeile myös toisia kuvatiedostoja annettujen sijaan, jos haluat.

Tehtävä: kuvan leikkaaminen toisen perusteella

Toinen tapa yhdistää kaksi kuvaa on käyttää yhtä kuvaa "muottina" tai "silhuettina" ja leikata toisesta kuvasta sen muotoinen pala.

../_images/combine2_input.png

Alkuperäiset.

../_images/combine2_output.png

Yhdistelmä.

Tiedostossa Task6.scala on tehtävä, jossa pääset toteuttamaan tämän.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Lisää kuvien yhdistelyä

Jos pidit edellisestä tehtävästä, saatat haluta leikkiä myös seuraavalla koodilla:

val photo   = Pic("kid.png").scaleBy(1.3)
val drawing = Pic("bird.png")
def isBright(color: Color) = color.intensity > 60
def selectColor(c1: Color, c2: Color) = if (isBright(c2)) Black else c1
photo.combine(drawing, selectColor).show()
intensity-metodin palauttama arvo kertoo värin kirkkaudesta. Esimerkiksi puhtaan valkoisen intensity on 255 ja puhtaan mustan nolla.

Miltä koodin tuottama kuva näyttää ja miksi?

Mitä tapahtuu, jos laitat kirkkausrajaksi jotakin muuta kuin 60? Kokeile vaikkapa arvoja 20 ja 200.

Mitä tapahtuu, jos vaihdat c1 ja c2 keskenään selectColor-funktion rungossa?

Kokoelmankäsittelytehtäviä

Luvun päättävissä ohjelmointitehtävissä laadit työkaluja kokoelmien käsittelyyn. Nämä tehtävät eroavat aiemmista luvuista merkittävästi sikäli, että niissä et ainoastaan kutsu valmiina annettua korkeamman asteen funktiota antaen sille toisen funktion parametriksi, vaan myös toteutat itse uusia korkeamman asteen funktioita.

Nämäkin tehtävät löytyvät HigherOrder-projektista.

Tehtävä: repeatForEachElement

Tässä tehtävässä toteutat korkeamman asteen funktion, jolla parametrifunktion mukaisen toimenpiteen voi toistaa kullekin lukuja sisältävän vektorin alkiolle. Sille on valmiiksi määritelty parametrimuuttujat ja pari käyttötapausta tiedostoon Task7.scala, mutta toteutus puuttuu.

Tehtäväsi on täydentää funktio niin, että se toimii kuvatulla tavalla. Tällöin myös annetun koodin lopussa olevat käyttötapaukset alkavat toimia ja tuottavat kommenteissa mainitut tulosteet.

Ohjeita ja vinkkejä:

  • repeatForEachElement-funktion toinen parametri on funktio, joka on tyyppiä Int => Unit. Niinpä esimerkiksi printCube ja printIfPositive ovat sopivia parametreja repeatForEachElement-funktiolle.
  • Voit käyttää for-silmukkaa.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Tehtävä: transformEachElement

Tässä tehtävässä toteutat korkeamman asteen funktion itse kokonaan. Funktion tulee muokata annettua puskuria siten, että se käyttää parametrina saamaansa muunnosfunktiota korvatakseen kunkin alkion uudella.

(Ajatus on siis samansukuinen kuin kuvien transformColor-metodissa edellä, mutta tässä laadittava funktio muokkaa alkuperäistä kokoelmaa, ei tuota uutta.)

Löydät tarkemman tehtävänannon tiedostosta Task8.scala.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Tehtävä: turnElementsIntoResult

Tässä tehtävässä toteutat ensin vielä yhden korkeamman asteen funktion ja sitten sille pari käyttötapausta.

Tiedostoon Task9.scala on merkitty paikka funktiolle turnElementsIntoResult, joka sinun pitäisi toteuttaa tehtävän ensimmäisessä vaiheessa. Sen alla on myös valmiina yksi käyttötapaus.

Vinkki: Käytä for-silmukkaa sekä kokoojamuuttujaa, jolla pidät kirjaa tuloksesta. Aluksi kannattaa myös katsoa seuraava graafinen esitys, joka kuvailee, miten turnElementsIntoResult-funktion pitäisi toimia.

Kun summan laskeminen turnElementsIntoResultin avulla onnistuu, siirry tehtävän toiseen vaiheeseen. Suunnittele ja toteuta funktiot positiveCount ja productOfNonZeros ja käytä niitä positiivisten lukujen laskemiseen ja alkioiden tulon muodostamiseen.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Kokoelmista ja korkeamman asteen funktioista

Äskeisissä tehtävissä laadit korkeamman asteen funktioita, joilla voi tehdä monenlaisia asioita alkiokokoelmalle: toistaa tietyn toiminnon kullekin alkiolle, muuntaa kunkin alkion toiseksi tietyllä muunnosfunktiolla, tai muodostaa tuloksen kaikkien alkioiden ja parametriksi annetun funktion perusteella. Näitä funktioita käyttämällä voi laatia kokoelmia käsitteleviä ohjelmia ilman, että joutuu kirjoittamaan silmukoita: välitetään vain suoritettava toimenpide parametriksi funktiolle, joka huolehtii sen toistamisesta.

Koska tällaiset funktiot ovat käytännöllisiä, on niitä muistuttavia työkaluja tarjolla Scala API:ssa. Aivan pian luvussa 6.2 näet, että Scalan valmiilla kokoelmatyypeillä on koko liuta korkeamman asteen metodeita, joilla kokoelmien sisältöä voi käsitellä erittäin joustavasti. Osa noista metodeista muistuttaa kovastikin näissä tehtävissä laatimiasi.

Puisevaa deffata noin paljon funktioita!

Et ehkä usko tuossa esitettyä väitettä siitä, että voisi olla kätevämpää käsitellä kokoelmia tällaisilla korkeamman asteen funktioilla kuin silmukoilla. Eikö ole nakertavaa joutua erikseen määrittelemään ja nimeämään parametreiksi välitettävät funktiot (esim. printIfPositive)?

On totta, että parametriksi välitettävien funktioiden nimeäminen voi olla epäkäytännöllistä. Onneksi helpotusta on tiedossa heti seuraavassa luvussa 6.1, jossa puhutaan nimettömistä funktioista.

Yhteenvetoa

  • Funktiotkin ovat dataa. Niitä voi muun muassa tallentaa muuttujiin ja välittää parametreiksi toisille funktioille.
  • Funktioita käsittelevillä funktioilla eli korkeamman asteen funktioilla voi toteuttaa erittäin yleishyödyllisiä toimintoja: yleiskäyttöiselle funktiolle voidaan välittää parametriksi toinen funktio, joka täsmentää sitä, mitä halutaan tehdä.
  • Alkiokokoelmia voi laittaa sisäkkäin. Tämä on eräs tapa kuvata kaksiulotteista tai moniulotteista tietoa.
  • Lukuun liittyviä termejä sanastosivulla: korkeamman asteen funktio; parametriluettelo; suodin.

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 Riku Autio, Jaakko Kantojärvi, Teemu Lehtinen, 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.

Lisäkiitokset tähän lukuun

Harmaasävytehtävä on ideoitu Jessen Havillin samanteemaisen tehtävän perusteella.

Piilokuvatehtävät on kehitetty Nick Parlanten muotoileman ja David J. Malanin alun perin ideoiman tehtävän pohjalta.

Kuvien keskiarvo -tehtävän kaksi kuvaa ovat Daniel Cookin tekemiä ja Creative Commons Attribution 3.0 -lisenssillä julkaisemia.

Värimuutoksen koki Akseli Gallén-Kallelan maalaus.

../_images/imho5.png
Palautusta lähetetään...