Luku 6.1: Funktioita parametreina

../_images/person09.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 paluuarvoina.

Ennen kuin käsittelemme sitä, miksi tämä on erittäinkin hyödyllistä, katsotaan konkreettisia pikkuesimerkkejä.

Funktion sijoittaminen muuttujaan

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 vahvistaa, 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 paluuarvon tyyppi.

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

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

Tuonsorttista olemme tehneet ennenkin, mutta seuraavaa emme. Määritellään muuttuja, joka viittaa yhteen funktioistamme:

var erasFunktio = tuplaaerasFunktio: Int => Int = Lambda$1338/0x00000008010c5400@65d90b7f
erasFunktio(10)res2: Int = 20

Sijoitamme erasFunktio-muuttujan arvoksi lausekkeen tuplaa arvon. Huomaa: tässä emme kutsu tuplaa-funktiota; emme välitä sille parametrilukua! Nimi erasFunktio viittaa nyt funktioon, joka palauttaa kaksinkertaisen luvun.

REPLkin ilmoittaa, että muuttujamme tyyppi on Int => Int eli luvun ottava ja luvun palauttava funktio.

REPLin kuvaus muuttujan arvosta — muuttujaan tallennetusta funktiosta — on karsean näköinen. Mutta älä välitä...

... funktio toimii kyllä! erasFunktio(10) kutsuu sitä funktiota, johon erasFunktio-muuttuja viittaa.

erasFunktio on muuttuja siinä missä muutkin. Koska muuttujamme sattuu olemaan var, niin voimme vaikka vaihtaakin sen arvoa kokeeksi. Pistetään se nyt viittaamaan seuraava-funktioomme:

erasFunktio = seuraavaerasFunktio: Int => Int = Lambda$1508/0x0000000801120600@260f05ee
erasFunktio(10)res3: Int = 11

Keskeinen opetus: Funktiotkin ovat dataa, ja niitä voi sijoittaa muuttujiin. Niitä voi tällöin kutsua muuttujan nimellä.

Ja kun kerran funktioilta voi käsitellä kuin muutakin dataa, niitä voi myös välittää parametriksi toiselle funktiolle, kuten näet seuraavaksi.

Funktion välittäminen parametrina

Tavoite: kahdesti-funktio

Meillä on jo nämä kaksi funktiota, jotka kuvaavat sellaisia toimenpiteitä, jotka voi kohdistaa kokonaislukuun ja jotka tuottavat kokonaislukutuloksen.

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

Määritellään nyt funktio nimeltä kahdesti, jolla voi suorittaa minkä tahansa tuollaisen Int => Int -tyyppisen funktion kahteen kertaan. Tieto siitä, mikä funktio suoritetaan kahteen kertaan, välitetään kahdesti-funktiolle parametriksi. Olkoon tavoite, että kahdesti-funktiota voi käyttää näin:

kahdesti(seuraava, 1000)res4: 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)res5: Int = 4000

kahdesti-funktion toteutus

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 kutsuu ensimmäiseksi parametriksi saamaansa funktiota toiseksi parametriksi välitetylle kohdeluvulle. Sen tehtyään ja paluuarvon saatuaan se kutsuu samaa funktiota tuolle paluuarvolle uudestaan.

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.

Tyyppimerkintöihin 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

2) ottaa toiseksi parametrikseen kokonaisluvun

3) ja palauttaa kokonaisluvun.

Väliajatus

Vertaile keskenään:

  • Funktion voi 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 paluuarvoina, 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, mutta monissa kielissä voi myös laatia korkeamman asteen funktioita. Scalassakin tämä on mahdollista, kuten jo näit.

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 paluuarvoina 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ä:

Esimerkki 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.

Esimerkki 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.

Esimerkki 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.

Esimerkki 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)res6: Boolean = true
onkoJarjestyksessa("Haskell", "Java", "Scala", vertaaPituuksia)res7: Boolean = false
onkoJarjestyksessa("Java", "Scala", "Haskell", vertaaMerkkeja)res8: Boolean = false
onkoJarjestyksessa("Haskell", "Java", "Scala", vertaaMerkkeja)res9: Boolean = true
onkoJarjestyksessa("200", "123", "1000", vertaaIntArvoja)res10: Boolean = false
onkoJarjestyksessa("200", "123", "1000", vertaaPituuksia)res11: 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.5 AuctionHouse-luokkaa. Otetaan pari lisätavoitetta. Halutaan, että:

  1. AuctionHouse-olioilla on metodi, jolla voi pyytää luettelon kaikista sellaisista 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 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(val name: String):

  private val items = Buffer[EnglishAuction]()

  // ... muita metodeita ...

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

end AuctionHouse

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.5 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ä, kun tarkastetaan, täyttyykö hakukriteeri.

Nyt metodia voi käyttää vaikkapa näin:

def checkIfOpen(candidate: EnglishAuction) = candidate.isOpen

def checkIfHandbag(candidate: EnglishAuction) =
  candidate.description.toLowerCase.contains("handbag")

@main def findAllTest() =
  val house = AuctionHouse("ReBay")
  house.addItem(EnglishAuction("A glorious handbag", 100, 14))
  house.addItem(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.3 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.4: 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()

transformColors-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.

Bonuskikka: Parametrilausekkeiden nimeäminen kutsuvaan koodiin

Koodista saa joskus luettavampaa nimeämällä parametrit paitsi kutsuttuun koodiin myös kutsuvaan.

Katsotaan esimerkkinä swapGreenAndBlue-funktiota, jonka toteutimme edellä näin:

def swapGreenAndBlue(original: Color) =
  Color(original.red, original.blue, original.green)

Tämän koodin lukijan on kiinnitettävä erityistä huomiota parametrien järjestykseen. Hänen on muistettava, että Colorin toinen parametri on green ja kolmas blue, joten tässä original.blue päätyy uuden olion green-parametrin arvoksi ja original.green taas blue-parametrin arvoksi.

Voimme myös kirjata parametrien nimet tuohon koodiin näin:

def swapGreenAndBlue(original: Color) =
  Color(red=original.red, green=original.blue, blue=original.green)

Parametrilausekkeiden edessä lukee, minkä nimisen parametrin arvosta on kyse.

Tähän koodiin on nyt nimenomaisesti kirjattu, että blue menee greenin arvoksi ja toisin päin.

Käytetyt nimet eivät ole mielivaltaisia. On käytettävä juuri niitä parametrien nimiä, jotka kutsutussa koodissa on määritelty. Esimerkiksi tässä Color-luokka määrää, että kolme parametria ovat nimiltään red, green ja blue. (Tämä tieto löytyy esimerkiksi scaladoceista.)

Tuossa esimerkissä loimme olion ja nimesimme luontiparametreja. Parametrilausekkeita voi nimetä vastaavasti myös ihan tavalliseen metodikutsuun. Lisäesimerkkejä löytyy verkosta.

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 moduulin 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 näkyvä?

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. Silti 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.3) 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  then Red
  else if x + y < 200 then Black
  else if y % 10 < 5  then 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). 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: String) =
  eka * toka + kolmas * neljas.lengthkokeilu(eka: Int, toka: Int)(kolmas: Int, neljas: String): 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, "sana")res12: Int = 14
kokeilu(1, 2, 3, "sana")-- Error:
  |kokeilu(1, 2, 3, "sana")
  |              ^
  |too many arguments for method kokeilu: (eka: Int, toka: Int)(kolmas: Int, neljas: String): Int

On olemassa tapauksia, joissa usean parametriluettelon käyttö on kätevää. Aihetta ei kuitenkaan tällä kurssilla juurikaan käsitellä, eikä sinun tarvitse 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 tulee vastaan ihan kohta.

Valinnaista asiaa parametriluetteloista

Jos parametriluetteloja on useita, voi funktiota kutsua osittain (ns. partially applied function), kuten seuraavassa esimerkissä.

def kokeilu(eka: Int, toka: Int)(kolmas: Int, neljas: String) =
  eka * toka + kolmas * neljas.lengthkokeilu(eka: Int, toka: Int)(kolmas: Int, neljas: String): Int
kokeilu(1, 2)res13: (Int, String) => Int = Lambda$1449/0x00000008010dd800@45790cb

Kutsumme funktiota mutta annamme vain yhden luettelollisen parametriarvoja.

Saamme paluuarvona toisen funktion. Se on tyyppiä (Int, String) => Int, eli sillä on yksi parametriluettelo, jossa kaksi parametria. Tämä parametriluettelo vastaa kokeilu-funktion jälkimmäistä parametriluetteloa.

Osittain kutsutun funktion voi esimerkiksi poimia muuttujaan, kuten tässä:

val osittainLaskettu = kokeilu(1, 2)osittainLaskettu: (Int, String) => Int = Lambda$1467/0x00000008011055c8@626df173

Muuttujassa on kaksiparametrinen funktio, joka laskee 1 * 2 + param1 * param2.length.

Tätä funktiota voi kutsua tavalliseen tapaan:

osittainLaskettu(10, "sana")res14: Int = 42
osittainLaskettu(100, "laama")res15: Int = 502

Tässä välitettävät parametriarvot vastaavat alkuperäisen kokeilu-funktion parametreja kolmas ja neljas.

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)res16: 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 kullekin indeksille. Tässä tuplausfunktiota on kutsuttu luvuille 0–9.

Tässä vastaava esimerkki seuraava-funktiolla:

Buffer.tabulate(5)(seuraava)res17: 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 5.2).

Vektorillinen kasvavahkoja satunnaislukuja, ole hyvä:

import scala.util.Randomdef 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 metodilla on 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ä. Usein sisäkkäinen ratkaisu on kuitenkin kätevä.

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 do
  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 do
  for j <- lukuja(i).indices do
    if ehtoja(j) then
      tulos += lukuja(i)(j)
println(tulos)

Kirjoita tähän allekkain ja järjestyksessä kaikki ne eri luvut, jotka tulos-muuttujaan on tallennettuna tämän koodinpätkän suorituksen aikana. (Siis vain keskenään erisuuruiset luvut. Älä toista mitään 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
        (color1.opacity + color2.opacity) / 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) then 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, kirkkausraja on muu 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 merkittävästi aiemmista 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-moduulista.

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 loppuun kirjatut 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. Funktio ottaa parametreiksi puskurin ja muunnosfunktion; se käyttää tuota muunnosfunktiota kuhunkin puskurin alkioon ja korvaa kyseisen alkion muunnosfunktion palauttamalla uudella arvolla.

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

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

Jos puskurin päivittämisen kanssa tulee hankaluuksia joko ohjelmaa kääntäessä tai ohjelma-ajon aikana, jompikumpi alla olevista vinkeistä saattaa auttaa.

Jos kääntäjä ei hyväksy yritystäsi vaihtaa puskurin alkiota

Sait ehkä reassignment to val -virheilmoituksen tai vastaavan? Siinä tapauksessa kannattaa kerrata tämä toimimaton koodi luvusta 5.5.

def kasvataAlkioita(luvut: Buffer[Int]) =
  for luku <- luvut do
    luku = luku + 1

Koodissa on ensinnäkin periaatteellinen ongelma. luku-muuttujaan tulee automaattisesti alkio läpikäytävästä puskurista, mutta se on muuten ihan tavallinen Int-tyyppinen muuttuja. Se varastoi vain yhden poimitun luvun, ei viittausta puskuriin itseensä. Tuon muuttujan kautta ei siis pääse käsiksi puskuriin, josta luku on peräisin eikä sen kautta voi muuttaa puskurin sisältöä.

Lisäksi tuo koodi ei ole kelvollinen siksikään, että silmukkamuuttuja (tässä luku) on val eikä var. Siihen ei siis voi sijoittaa lainkaan, mistä kääntäjä valittaa.

Muokataksesi tiettyä puskurin alkiota, mainitse kyseinen puskuri ja kyseinen indeksi:

puskuri(indeksi) = uusiAlkio

Miten sitten saat käytyä läpi indeksit? Kumpi tahansa näistä luvun 5.6 esittelemistä tavoista toimii:

for indeksi <- kokoelma.indices do
  ...
for indeksi <- 0 until kokoelma.size do
  ...

Jos ohjelma kaatuu ajaessa, kun yrität vaihtaa puskurin alkiota

Olet ehkä tehnyt jotain tämän suuntaista, ja ruudulle on napsahtanut ConcurrentModificationException-virhe?

var indeksi = 0
for merkkijono <- puskuri do
  puskuri(indeksi) = uusiAlkio
  indeksi += 1

Tässä tulee vastaan eräs rajoitus. Nuolen <- oikealla puolella mainittua kokoelmaa ei saa muokata silmukan sisällä, tai syntyy mainittu virhetilanne. Tuossa, kun nuolen oikealla puolella on puskuri-muuttujan osoittama Buffer-olio, ei silmukan sisällä voi sijoittaa tuohon samaiseen puskuriin.

Tuo rajoitus ei ole läheskään niin ikävä kuin miltä se voi aluksi kuulostaa. Itse asiassa sen huomioiminen johtaa tässä vähän yksinkertaisempaan ratkaisuun.

Sen sijaan, että mainitset nuolen oikealle puolella kyseisen puskurin, käy läpi puskurin indeksit. Kumpi tahansa seuraavista toimii:

for indeksi <- puskuri.indices do
  puskuri(indeksi) = ...
for indeksi <- 0 until puskuri.size do
  puskuri(indeksi) = ...

Nyt silmukka käy läpi Range-oliota eikä puskuria itseään, ja mitään ongelmaa ei ole. Et myöskään tarvitse "omaa var-muuttujaa" indeksille.

(Sivuhuomio: Mainitun rajoituksen taustalla on silmukoiden toteutustapa ns. iteraattoreilla; luku 11.3. Iteraattoreilla voi käydä näppärästi läpi mitä erilaisimpia tietorakenteita, mutta niiden toimintaa ei voi yleisessä tapauksessa taata, jos sallitaan iteroitavan rakenteen samanaikainen muokkaus.)

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.

Seuraava graafinen esitys kuvailee, miten turnElementsIntoResult-funktion pitäisi toimia. (Tässä esimerkissä annetaan turnElementsIntoResultsille parametriksi addToSum-funktio, joka esiintyy myös tehtävän ensimmäisessä vaiheessa.)

Vinkki ykkösvaiheeseen

Käytä for-silmukkaa sekä kokoojamuuttujaa, jolla pidät kirjaa tuloksesta.

Jatkovinkki ykkösvaiheeseen

Pidä viimeisin välitulos tallessa kokoojamuuttujassa. Käy läpi kokoelman alkiot ja sovella parametrina saatua toimenpidettä aina viimeisimpään välitulokseen ja käsiteltävään alkioon. Toimenpiteen tuottamasta arvosta saat uuden välituloksen.

Kun summan laskeminen turnElementsIntoResultin avulla onnistuu, siirry tehtävän toiseen vaiheeseen. Suunnittele ja toteuta funktiot addAbsolute, positiveCount ja productOfNonZeros ja käytä niitä turnElementsIntoResult-funktion parametreina. Näin saat summattua alkioiden itseisarvot, laskettua kokoelman positiiviset luvut ja muodostettua alkioiden tulon.

Vinkki toiseen vaiheeseen: itseisarvojen summa

addAbsolute-funktion pitäisi tehdä ihan sama kuin addToSum-funktionkin, paitsi että jälkimmäisen luvun sijaan summaan lisätään tuon luvun itseisarvo. (luku.abs toimii.)

Aiemmasta summasta ei tarvitse (eikä pidä) laskea itseisarvoa.

Vinkki toiseen vaiheeseen: positiiviset luvut

Positiiviset luvut voi laskea näin: Aluksi ei ole löydetty yhtään positiivista lukua. Kunkin alkion kohdalla tarkistetaan, onko se positiivinen ja muodostetaan uusi välitulos. Positiivisen alkion kohdalla välitulos on edellistä isompi, muuten ei.

turnElementsIntoResult, jonka olet jo määritellyt, hoitaa läpikäynnin. Nyt laadittavan funktion positiveCount tulee hoitaa äsken mainittu osatehtävä: se tuottaa edellisen välituloksen ja yhden alkion perusteella seuraavan välituloksen, joka on edellisen välituloksen kanssa yhtäsuuri tai sitä yhdellä suurempi.

Vinkki toiseen vaiheeseen: lukujen tulo

Nollasta poikkeavien lukujen tulon voit laskea samaan tapaan kuin positiivisten lukujen määränkin.

Tällä kertaa välitulosta ei kasvateta nollalla tai yhdellä vaan kerrotaan käsiteltävällä alkiolla (ellei alkio ole nolla, jolloin se ei vaikuta tulokseen).

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 APIssa. Aivan pian luvussa 6.3 näet, että Scalan valmiilla kokoelmatyypeillä on koko liuta korkeamman asteen metodeita, joilla kokoelmien sisältöä voi käsitellä 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 on joskus epäkäytännöllistä. Onneksi helpotusta on tiedossa heti seuraavassa luvussa 6.2, 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ä.

  • Alkiokokoelmat voivat olla 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!

Materiaalin luvut tehtävineen ja viikkokoosteineen on laatinut Juha Sorva.

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, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Joel Toppinen, Anna Valldeoriola Cardó ja Aleksi Vartiainen.

Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.

Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista suunnittelivat Juha Sorva ja Teemu Sirkiä. Teemu Sirkiä ja Riku Autio toteuttivat ne apunaan Teemun aiemmin rakentamat työkalut Jsvee ja Kelmu.

Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset laati Juha Sorva.

O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen, Juha Sorva ja Jaakko Nakaza. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.

Tapa, jolla 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+ luotiin alun perin Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Nykyään tätä avoimen lähdekoodin projektia kehittää Tietotekniikan laitoksen opetusteknologiatiimi ja tarjoaa palveluna laitoksen IT-tuki; sitä ovat kehittäneet kymmenet Aallon opiskelijat ja muut.

A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen suunnitteluun ja toteutukseen on osallistunut useita opiskelijoita yhteistyössä O1-kurssin opettajien kanssa.

Kurssin tämänhetkinen henkilökunta löytyy luvusta 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.

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