Luku 7.1: Lisää kokoelmia, lisää ohjelmia
Tulosarvon tuottaminen kokoelman sisällön perusteella
foldLeft
-metodi
Luvun 6.1 päättäneessä turnElementsIntoResult
-tehtävässä työstimme ajatusta siitä,
että kokoelman perusteella voidaan muodostaa yksi tulos (esim. kokoelman alkioiden summa
tai tulo) soveltamalla alkioihin tiettyä funktiota toistuvasti. Tarkemmin sanoen:
sovelletaan tiettyä kaksiparametrista funktiota (esim. yhteenlaskua) ensin johonkin
alkuarvoon ja kokoelman ensimmäiseen alkioon, sitten näin saatuun välitulokseen ja
kokoelman toiseen alkioon jne.
Tällainen hyvinkin yleiskäyttöinen metodi on määritelty myös Scala-kokoelmille. Sen nimi
on foldLeft
. Seuraava käsky on eräs tapa laskea kokoelman sisältämien lukujen summa:
val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20) luvut.foldLeft(0)( (sumSoFar, next) => sumSoFar + next )res0: Int = 4
foldLeft
-metodilla on kaksi parametriluetteloa (kuten tabulate
-metodillakin; luku 6.1):
Toisessa on funktio, jolla yhdistetään välitulos ja seuraava alkio. Tässä esimerkissä kyseessä on yksinkertainen summafunktio.
Tässä luvusta 6.1 toisintona (päivitetyllä termistöllä) graafinen esitys, joka kuvaa
foldLeft
-metodin toiminnan:
Summaavan foldLeft
-kutsun voi kirjoittaa yksinkertaisemminkin. Alla on myös esimerkki
alkioiden tulon laskemisesta:
luvut.foldLeft(0)( _ + _ )res1: Int = 4 luvut.foldLeft(1)( _ * _ )res2: Int = -20000
Ja tässä parametrifunktio summaa ensimmäiseen parametriinsa (välitulokseen) toisen parametrin (seuraavan alkion) toisen potenssin. Lopputuloksena saadaan alkioiden neliöiden summa:
luvut.foldLeft(0)( (sum, next) => sum + next * next )res3: Int = 566
Sivumaininta: helpompi tapa laskea summa tai tulo
Koska tässä luvussa on puhe korkeamman asteen funktioista, niin edellä "unohdettiin" eräs seikka.
Kokoelman alkioiden summan laskeminen on kovin yleinen toimenpide, ja Scala API tarjoaa
valmiin metodin. Saat alkioiden summan kutsumalla kokoelmaolion sum
-metodia: luvut.sum
.
Tämän metodin toimiminen edellyttää, että alkiot ovat sopivan tyyppisiä (lukuja).
Vastaavasti product
-metodilla voi laskea alkioiden tulon.
Kaikkiin ongelmiin ei kuitenkaan ole yhtä helppoa valmista ratkaisua tarjolla kuin
yhteen- ja kertolaskuun. Kuten jo näit, foldLeft
-metodilla voi paitsi laskea summan
myös muodostaa muunlaisia tuloksia alkioiden perusteella.
Lisää fold
-esimerkkejä: alkioiden tutkiminen
Yllä tulos oli aina samaa tyyppiä kuin kokoelman alkiotkin (tässä Int
). Tämä ei ole
välttämätöntä. Voidaan esimerkiksi muodostaa lukujen perusteella Boolean
-tyyppinen
tulos.
Seuraavat foldLeft
-kutsut etsivät kokoelmasta tietynlaisia lukuja yllä käsitellyn
exists
-metodin tapaan.
Tämä käsky vastaa käskyä luvut.exists( _ < 0 )
:
luvut.foldLeft(false)( (foundYet, next) => foundYet || next < 0 )res4: Boolean: true
Alkuarvona on false
. Tämä on se tulos, joka saataisiin, jos
vektorissa ei olisi alkioita lainkaan. (Tällöinhän ei ole myöskään
negatiivista alkiota.)
Ensimmäinen parametrifunktion parametreista on siis tässä
Boolean
-tyyppinen. Tämä välitulos kertoo, onko negatiivista
lukua jo löydetty.
Funktio palauttaa true
, jos ja vain jos negatiivinen luku oli
löydetty jo aiemmin tai nyt tarkasteltava luku on negatiivinen.
Tässä lyhyempi versio edellisestä sekä esimerkki, jossa saadaan false
-tulos:
luvut.foldLeft(false)( _ || _ < 0 )res5: Boolean = true luvut.foldLeft(false)( _ || _ < -100 )res6: Boolean = false
Lisää fold
-esimerkkejä: kuvien yhdisteleminen
Seuraavassa esimerkissä muodostetaan kuvia yhdistämällä pienempiä kuvia. Mitä koodi tekee ja millaisia yhdistelmiä syntyy? Päättele, ennusta ja kokeile.
val base = triangle(50, 50, Yellow)base: Pic = triangle-shape val circles = (1 to 255).map( n => circle(n, Color(n, 0, 255)) )circles: IndexedSeq[Pic] = Vector(circle-shape, circle-shape, circle-shape, circle-shape, circle-shape, [...]) val artwork1 = circles.foldLeft(base)( _.onto(_) )artwork1: Pic = combined pic
val artwork2 = (1 to 50).foldLeft(circle(50, Red))( (result, n) => result.leftOf(circle(n, Black)) )artwork2: Pic = combined pic
Ja ehkä muistat vielä placeStar
-funktion, jonka toteutit Stars-ohjelmaan luvussa 4.4
ja joka asettelee yhden tähden taivasta vasten? Asiasta ei silloin ollut puhetta, mutta
Stars-ohjelmaan valmiiksi kirjoitettu koodi muodostaa koko tähtitaivaan kuvan nimenomaan
foldLeft
-käskyllä, joka toistuvasti kutsuu laatimaasi funktiota näin:
def create(skyData: StarMap, bgSize: Int) =
val darkSky = rectangle(bgSize, bgSize, Black)
skyData.stars.foldLeft(darkSky)(placeStar)
Vielä abstraktiotasoista
Pikkutehtäviä: foldLeft
reduceLeft
-metodi
Metodi reduceLeft
muistuttaa foldLeft
iä kovasti. Eroina ovat:
reduceLeft
ille ei erikseen tarvitse antaa alkuarvoa, vaan ensimmäisenä tuloksena käytetään kokoelman ensimmäistä alkiota.reduceLeft
ei toimi tyhjälle kokoelmalle lainkaan, vaan aiheuttaa ajonaikaisen virhetilanteen, jos sitä kutsutaan tyhjälle kokoelmalle.reduceLeft
in palauttama lopputulos on (ja välituloksetkin ovat) aina samaa tyyppiä kuin kokoelman alkiotkin.
Käytetään kokeeksi reduceLeft
-metodia pienimmän alkion selvittämiseen:
val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20) luvut.reduceLeft( min(_, _) )res7: Int: -20
Tai vielä lyhyemmin:
luvut.reduceLeft(min)res8: Int: -20
reduceLeft
on kätevä silloin, jos 1) voidaan olettaa, että kokoelmassa on ainakin yksi
alkio, ja 2) yksialkioisen kokoelman tapauksessa tulokseksi kelpaa kokoelman ainoa alkio
sellaisenaan.
Tässä lisäesimerkki kuvilla:
val kuvat = Vector(Pic("face.png"), Pic("horse.png"), Pic("obstacle.png"))kuvat: Vector[Pic] = Vector(face.png, horse.png, obstacle.png) val allekkain = kuvat.reduceLeft( _.above(_) )allekkain: Pic = combined pic
Termeistä
Verbin fold käyttö juontuu siitä, että kyse on ikään kuin
kokoelman sisältämien arvojen rimpsun "taittelemisesta"
kasaan yhdeksi lopputulokseksi. Nimen taustalla on muutakin,
mutta yksi tapa ajatella Scalan foldLeft
-toteutusta on, että
se aloittaa läpikäynnin "vasemmalta" eli kokoelman alusta ja
etenee vaiheittain "oikealle".
Tehtävä: selvitä, mitä tekevät Scalan metodit foldRight
ja
reduceRight
.
reduceLeft
plus Option
— ja favorite
taas
Yllä todettiin reduceLeft
-metodin rajoitukseksi se, että
kokoelmassa tulisi olla ainakin yksi alkio, koska muuten
tulosta ei ole määritelty.
Toisaalta olet jo oppinut käyttämään Option
-luokkaa kuvaamaan
tilanteita, joissa jokin tulos joko on tai ei ole olemassa.
Nämä ajatukset voi yhdistää: olkoon tulos None
, jos alkioita ei
ole ja muussa tapauksessa sovelletaan reduceLeft
iä ja kääritään
sen paluuarvo Some
-kääreeseen.
Kokeillaan tätä palaamalla vielä GoodStuff-ohjelman Category
-luokkaan
ja sen suosikkikirjanpitoon, josta on esitetty versioita luvuissa 4.3
ja 5.5. Tässä taas yksi versio:
def favorite =
if this.experiences.isEmpty then None else Some(this.experiences.reduceLeft( _.chooseBetter(_) ))
Ilman if
-käskyllä toteutettua tarkistusta reduceLeft
aiheuttaisi ajonaikaisen virhetilanteen (vrt. luvun 4.2
NullPointerException
).
Tarve yhdistää reduceLeft
-kutsu ja Option
-kääre on sen
verran yleinen, että tarkoitukseen on tehty oma metodinsa.
Varsin näppärä toteutus favorite
-metodille syntyy tätä
reduceLeftOption
-metodia käyttämällä:
def favorite = this.experiences.reduceLeftOption( _.chooseBetter(_) )
Tämä on käytännössä lyhennysmerkintä edelliselle versiolle,
jossa käytettiin if
iä, Option
ia ja reduceLeft
iä
erikseen.
Vapaaehtoinen lisätehtävä: MapReduce
Tässä ja edellisessä luvussa ovat esiintyneet map
- ja reduce
-
nimiset metodit, jotka muiden esiteltyjen ohella ovat tyypillisiä
funktionaaliselle ohjelmointitavalle (mistä lisää luvussa 11.2).
Selvitä ainakin pääpiirteissään, mikä on MapReduce-ohjelmointitapa, jota mm. Google-yhtiö on käyttänyt valtavien datamäärien käsittelyyn, ja mikä on sen yhteys kohtaamiimme kokoelmankäsittelymetodeihin.
Tehtävä: Election-ratkaisua uusiksi (osa 2/2)
Edellisessä luvussa 6.3 uudistit pari District
-luokan metodia. Uudista nyt kaksi
lisää: totalVotes
-nimiset metodit. Toteuta ne silmukan sijaan käyttämällä joko
foldLeft
iä tai map
- ja sum
-metodeita. Tai mieluiten mieti vaikka molemmat
toteutustavat.
Jos taannoin laadit totalVotes
eille avuksi countVotes
-metodin, kuten luvussa 5.6
ehdotettiin, niin kohdista muutos siihen.
District
-luokasta edelleen puuttumaan jäävät metodit toteutetaan aikanaan luvussa 10.1.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
FlappyBug-tehtävä (osa 17/17: koodia siistimmäksi)
Tutkikaamme myös FlappyBug-ohjelmaan luvussa 5.6 toteutettuja metodeja sillä silmällä, miten niitä voisi uudistaa korkeamman asteen metodeilla.
Uudista seuraavat metodit. Metodien tulee tehdä sama kuin ennen, mutta yksinkertaista
toteutuksia korvaamalla for
-silmukat korkeamman asteen metodikutsuilla. Voit ottaa
pohjaksi oman aiemman ratkaisusi tai taannoisen esimerkkiratkaisun.
timePasses
luokastaGame
. Käytäforeach
-metodia (luku 6.3).isLost
samasta luokasta. Toteutus yksinkertaistuu huomattavasti luvun 6.3 esittelemällä metodilla, jolla voi nimensä mukaisesti suoraan tutkia, onko ehdon toteuttavaa estettä olemassa.Käyttöliittymän
makePic
-metodi. Korvaa silmukkafoldLeft
-metodikutsulla, joka sijoittaa kunkin esteen paikoilleen.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Jälkihuomio 1: abstraktiotasoista
Edellisen luvun 6.3 Snake-tehtävässä pärjäsit placeCopies
-metodilla,
koska kaikki madon segmentit piirrettiin samannäköisinä. Käytössäsi
oli siis korkean abstraktiotason työkalu, joka hoiti homman.
Äskeisessä tehtävässä taas toivottavasti käytit makePic
iin
foldLeft
iä, kuten pyydettiin. placeCopies
ei olisikaan
kelvannut, koska esteiden kuvat eivät olleet keskenään identtisiä;
valmiin työkalun puuttuessa turvauduit matalamman abstraktiotason
yleiskäyttöisempään työkalustoon.
Jos o1
-kirjasto ei olisi sattunut tarjoamaan placeCopies
-metodia,
niin myös matotehtävän olisi toki voinut ratkaista myös
foldLeft
illä. (Kokeile, jos haluat.)
Itse asiassa kirjaston ohjelmakoodissa placeCopies
on toteutettu
nimenomaan foldLeft
illä, joten epäsuorasti käytit sitä joka
tapauksessa.
Jälkihuomio 2: jatkoa tehokkuuspohdinnoille
Käytit toivottavasti isLost
-metodissa exists
-metodia (tai
kenties forall
ia).
Luvun 5.6 vapaaehtoisessa osiossa pohdittiin tehokkuutta: tekikö vanha versio periaatteessa turhaa työtä käymällä koko vektorin läpi vaikka olikin jo löytynyt ehdon täyttävä este?
exists
ja forall
keskeyttävät läpikäynnin, kun paluuarvo
on selvillä, joten niiden käyttäminen on yksi tapa välttää tuo
(tässä mitätön ja vain periaatteellinen) tehokkuusongelma.
Emme palaa FlappyBugin pariin enää pisteytetyissä tehtävissä, mutta voit itse kehitellä peliä pidemmälle, jos haluat.
Kokoelmametodien yhdistelyä
Ohjelmia kirjoittaessa tulee usein vastaan tilanteita, joissa on hyödyksi yhdistellä kokoelmien metodeita. Tulet näkemään tästä monia esimerkkejä jo tälläkin kurssilla.
Tässä yksi pikkuesimerkki lukuvektorilla. Seuraava käsky laskee sellaisten positiivisten alkioiden lukumäärän, joiden toinen potenssi on parillinen:
val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20) luvut.filter( _ > 0 ).map( n => n * n ).count( _ % 2 == 0 )res9: Int = 2
Seuraava pikkutehtävä tarkastelee metodien yhdistelemistä yleisemmällä tasolla:
Noista imperatiivisen muodon muuttaminen funktionaaliseksi -tehtävistä [kuten äskeisessä] käy kyllä hyvin ilmi, miten tuollainen [alkuperäisen silmukkakoodin] sekoitus eri konditionaalioperaattoreita onnistuu sekoittamaan pään. Tuo on varmaan juuri sitä mihin funktionaalisten metodeiden verbien sanallisuus auttaa.
Sadetehtävä
Johdanto
Kuvitellaan, että käytössä on mittari, joka tuottaa Vector[Int]
-muotoisia raportteja
sademääristä seuraavasti:
Vektorissa esiintyvät nollat ja positiiviset kokonaisluvut edustavat sademäärämittausten tuloksia.
Vektoriin voi olla tallennettuna yksi tai useampia mittausjaksoja. Kunkin jakson lopuksi siellä on erikoisarvo 999999, joka ei ole mittausdataa vaan ainoastaan ilmaisee jakson päättyvän.
Lisäksi vektorissa voi olla negatiivisia lukuja, jotka ovat muuta dataa eivätkä kerro sademääristä.
Tehtävänanto
Laadi HigherOrder-moduulin tiedostoon rainfall.scala
funktio averageRainfall
, joka
vastaanottaa ainoaksi parametrikseen kuvatunlaisen lukuvektorin. Funktio palauttaa
keskimääräisen sademäärän laskettuna ensimmäisestä parametrivektorissa esiintyvästä
mittausjaksosta. Funktion on toimittava seuraavien esimerkkien mukaisesti.
averageRainfall(Vector(40, 0, 50, 999999))res10: Option[Int] = Some(30) averageRainfall(Vector(0, 0, 0, 0, 999999))res11: Option[Int] = Some(0) averageRainfall(Vector(999999))res12: Option[Int] = None
Keskiarvo tulee palauttaa Option
iin käärittynä, Int
-tyyppisenä.
Luku 999999 vain päättää mittausjakson, eikä sitä lasketa mukaan keskiarvoon. Keskiarvo lasketaan sitä edeltävistä luvuista.
Jos mittaustuloksia ei ole, ei keskiarvoakaan voi laskea.
Jos vektorissa on useita mittausjaksoja, vain ensimmäinen huomioidaan:
averageRainfall(Vector(50, 150, 100, 100, 999999, 20, 30, 90, 999999, 0, 0, 999999))res13: Option[Int] = Some(100) averageRainfall(Vector(999999, 50, 100, 999999))res14: Option[Int] = None
Keskiarvo lasketaan vain ensimmäisestä jaksosta.
Ensimmäistä 999999:ää seuraavilla luvuilla ei ole merkitystä.
Jos ensimmäisessä jaksossa ei ole mittaustuloksia, ei saada tulosta.
Negatiiviset arvot eivät saa vaikuttaa tulokseen lainkaan:
averageRainfall(Vector(-10, 50, -100, 150, -1, -5, 100, 100, 999999, 20, 30, 90, 999999, 0, 0, 999999))res15: Option[Int] = Some(100) averageRainfall(Vector(-100, -5, -10, 999999, 50, 100, 999999))res16: Option[Int] = None
Ohjeita ja vinkkejä
Tavallinen kokonaislukujen jakolasku (luku 1.3) riittää tässä. Älä ryhdy pyöristämään "oikein".
Käytä korkeamman asteen metodeita. Vaihtoehtoja on monia. Mitä erilaisia ratkaisutapoja keksit?
Voit vertailun vuoksi tehdä myös silmukoihin perustuvan ratkaisun. Kummanlainen ratkaisu ilmaisee ohjelman tarkoituksen paremmin?
Voit olettaa, että syötevektori päättyy aina lukuun 999999.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Pieni jatkotehtävä edelliselle
Äskeinen averageRainfall
-funktio sai parametrikseen vektorillisen kokonaislukuja.
Entäpä jos käsiteltävä kokoelma koostuisikin merkkijonoista, joista osa on kelvollisia
ja osa ei? (Ehkä ne olisivat käyttäjän näppäilemiä tai peräisin jostakin datatiedostosta.)
Laadi samaan tiedostoon funktio averageRainfallFromStrings
, joka ottaa parametrikseen
vektorillisen merkkijonoja. Se tulkitsee merkkijonot kokonaisluvuiksi heittäen pois kaikki
alkiot, jotka eivät vastaa kelvollisia Int
-arvoja. Se laskee ja palauttaa keskiarvon
jäljelle jääneiden lukujen perusteella aivan kuin averageRainfall
.
Esimerkkejä:
averageRainfallFromStrings(Vector("cat", "50", "-100", "dog", "10", "999999", "20", "ten", "999999"))res17: Option[Int] = Some(30) averageRainfallFromStrings(Vector("cat", "dog", "999999"))res18: Option[Int] = None averageRainfallFromStrings(Vector("cat", "-1", "dog", "999999"))res19: Option[Int] = None
Ohjeita ja vinkkejä:
Älä kopioi
averageRainfall
-funktion koodia! Luo merkkijonovektorin perusteellaInt
-vektori ja kutsu jo tekemääsiaverageRainfall
ia.Voit olettaa, että merkkijono
"999999"
löytyy vektorista.Jos haluat lisävinkkejä työkaluihin, joilla tehtävä ratkeaa erittäin yksinkertaisesti, voit etsiä niitä luvusta 6.3 ja seuraavasta REPL-sessiosta:
"123".toIntOptionres20: Option[Int] = Some(123) "sata".toIntOptionres21: Option[Int] = None val possibleNumbers = Vector(Some(10), None, Some(15), None, None, Some(-5))possibleNumbers: Vector[Option[Int]] = Vector(Some(10), None, Some(15), None, None, Some(-5)) possibleNumbers.flattenres22: Vector[Int] = Vector(10, 15, -5) val words = Vector("one", "2", "3", "four")words: Vector[String] = Vector(one, 2, 3, four) words.map( _.toIntOption )words: Vector[Option[String]] = Vector(None, Some(2), Some(3), None)
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Kuivuustehtävä
Tehtävänanto
Toteuta samaan tiedostoon myös funktio drySpell
, joka
ottaa ensimmäisenä parametrinaan vastaavan kokonaislukuja sisältävän sadetietovektorin kuin
averageRainfall
,ottaa toisena parametrinaan positiivisen kokonaisluvun
length
, joka kertoo etsittävän "kuivan kauden" pituuden,etsii vektorista ensimmäisen sellaisen pätkän, jossa vähintään
length
peräkkäistä alkiota ovat suuruudeltaan välillä 0–5,palauttaa löydetyn kuivan kauden ensimmäisen indeksin (jos löytyi) tai negatiivisen luvun (jos ei löytynyt), ja
huomioi kaikki mittausjaksot ja kaikki vektorin sisältämät luvut, eikä katkaise tarkastelua lukuun 999999. Kuitenkaan kuiva kausi ei voi ulottua mittausjakson rajana toimivan luvun 999999 molemmille puolille. Myös negatiivinen luku katkaisee kuivan kauden.
Esimerkki:
val exampleData = Vector(10, 0, 3, 999999, 2, -10, 2, 0, 2, 1, 20, 1, 0, 1, 999999)exampleData: Vector[Int] = Vector(10, 0, 3, 999999, 2, -10, 2, 0, 2, 1, 20, 1, 0, 1, 999999) drySpell(exampleData, 3)res23: Int = 6
Kun etsitään vähintään kolmen mittauksen keskeytymätöntä kuivaa kautta...
... ensimmäinen sellainen löytyy indeksiltä kuusi.
Ohjeita, vinkkejä ja uusia työkaluja: indexWhere
, sliding
Voit olettaa, että dataa ei ole tolkuton määrä eikä tehokkuutta tarvitse optimoida.
Käytä jo esiteltyjä kokoelmametodeita. Opettele lisäksi seuraavat ja hyödynnä niitä.
indexWhere
etsii ensimmäisen alkion, joka täyttää annetun ehdon. Esimerkkejä:
val kokeilu = Vector(10, 5, 0, 3, 0, 100, 50)kokeilu: Vector[Int] = Vector(10, 5, 0, 3, 0, 100, 50) kokeilu.indexWhere( _ <= 0 )res24: Int = 2 kokeilu.indexWhere( _ < 0 )res25: Int = -1
sliding
tuottaa tietyn mittaisia pätkiä alkuperäisestä kokoelmasta. Esimerkkejä:
val myVector = Vector(10, 5, 0, 3, 0, 100, 50)myVector: Vector[Int] = Vector(10, 5, 0, 3, 0, 100, 50) myVector.sliding(4).foreach(println)Vector(10, 5, 0, 3) Vector(5, 0, 3, 0) Vector(0, 3, 0, 100) Vector(3, 0, 100, 50) kokeilu.sliding(5).foreach(println)Vector(10, 5, 0, 3, 0) Vector(5, 0, 3, 0, 100) Vector(0, 3, 0, 100, 50) kokeilu.sliding(5).toVectorres26: Vector[Vector[Int]] = Vector(Vector(10, 5, 0, 3, 0), Vector(5, 0, 3, 0, 100), Vector(0, 3, 0, 100, 50))
Tarkemmin sanoen sliding
in palauttama "luettelo pätkiä" on ns. iteraattori, jolla voi
käydä tietyt kokoelman pätkät läpi. Pätkät ovat alkuperäisen kokoelman mukaisessa
järjestyksessä. Alkuperäinen kokoelma ei muutu.
Tuolla iteraattorilla pääsee käsiksi kuhunkin pätkään kerran (ja vain kerran!).
Iteraattorille voi kutsua monia tuttuja kokoelmametodeita kuten foreach
, toVector
ja
indexWhere
. Jos haluat käsitellä kokoelman pätkiä usealla eri metodilla, voit kopioida
pätkät esimerkiksi vektoriin. (Tässä tehtävässä se ei tosin ole välttämätöntä.)
Huomaa yllä annetussa esimerkissä, että sama alkio esiintyy useassa pätkässä. Esimerkiksi alkuperäisen vektorin neljäs alkio, luku 3, on ensimmäisen pätkän neljäs alkio, toisen pätkän kolmas, kolmannen pätkän toinen ja neljännen pätkän ensimmäinen.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
drySpell
ja laskentatehokkuus
Yllä todettiin, ettei tehokkuutta tarvitse tässä tehtävässä optimoida. Jos tarvitsisi, huomion voisi kiinnittää siihen, että REPL-esimerkissä vinkattu sinänsä elegantti ratkaisu tekee ylimääräistäkin työtä.
Noihin metodeihin perustuva ratkaisu muodostaa monta osavektoria, jotka ovat keskenään osin identtisiä. Sitten noita osavektoreita käsitellään yksi kerrallaan kokonaan erikseen, vaikka samat luvut olisikin jo osin käsitelty edellisessä osavektorissa.
Tehokkaammatkin ratkaisut ovat mahdollisia; tapoja on monia.
Yksi mahdollisuus on käyttää indexOfSlice
-metodia. Kiinnostuneet
voivat etsiä siitä lisätietoja verkosta ja pohtia, miten sitä
voisi tässä soveltaa. Myös vielä tehokkaampia ratkaisuja voi
ideoida.
Pohdintatehtäviä
Päätämme luvun parilla pulmalla, joissa tutkit annettuja Scala-koodeja ja arvioit miten ne toimivat, jos toimivat. Tehtävien kautta nousee esiin ohjelmointitekniikoita, joista on kasvavassa määrin hyötyä, kun etenemme.
Seuraavat tehtävät ovat tarkoituksella sellaisia, ettei kaikkia vastauksia voi aukottomasti päätellä aiemmin esitetystä materiaalista. Toisaalta vastaukset ovat varsin järkeenkäyviä. Muista lukea palaute vääristä ja oikeista vastauksista!
Pohdinta-aihe 1: funktio funktion sisällä
Pohdinta-aihe 2: paikalliset muuttujat
Muutama sana sulkeumista
def etsiIsot(vektori: Vector[Int], raja: Int) =
def lasketaanIsoksi(luku: Int) = luku > raja
vektori.filter(lasketaanIsoksi)
Esimerkkikoodimme näyttää ymmärrettävältä: etsitään vektorista sellaisia lukuja, joiden arvo ylittää tietyn muuttujan arvon. On "luonnollista", että koodi toimii.
Asia ei kuitenkaan ole itsestäänselvä. filter
hän suoritetaan
omassa kutsupinon kehyksessään etsiIsot
-funktiota vastaavan
kehyksen päällä, eikä funktioilla ole pääsyä alempien kehysten
paikallisiin muuttujiin (luku 1.8).
Vastaava yritys ei toimisikaan kaikissa ohjelmointikielissä, mutta monissa — kuten Scalassa — toimii. Miksi?
Koskapa koodi toimii, täytyy olla niin, että filter
-funktiolle
ei välity vain lasketaanIsoksi
-funktio sellaisenaan vaan myös pääsy
raja
-muuttujaan, jota tuo funktio käyttää. Tällaista funktion ja
yhden tai useamman siihen kytketyn ulkopuolisen muuttujan yhdistelmää
sanotaan sulkeumaksi eli klosuuriksi (closure). (Funktio
ikään kuin "sulkee sisälleen" itsensä ulkopuolella määriteltyjä
muuttujia.)
Scala-kääntäjä huolehtii sulkeuman luomisesta automaattisesti, kun käytät funktiossa muuttujia, jotka ovat määriteltyjä siinä ympäristössä, mihin funktion määrittelykin kirjoitetaan. Sulkeuman käyttö ohjelmassa onkin usein huomaamatonta ja intuitiivista.
Sulkeumat ovat eräs syy määritellä funktio toisen sisään.
Esimerkiksi lasketaanIsoksi
voi käyttää raja
-muuttujaa juuri
siksi, että se on määritelty etsiIsot
-funktion sisällä, ja
samasta syystä pääsy tuohon muuttujaan voidaan välittää sulkeumassa
filter
-metodilla.
Esimerkissämme määrittelimme nimellisen funktion lasketaanIsoksi
,
mutta sulkeumia syntyy myös funktioliteraaleja käyttäessä. Tässä
toinen versio äskeisestä esimerkistä:
def etsiIsot(vektori: Vector[Int], raja: Int) =
vektori.filter( _ > raja )
Välitämme filter
-metodille parametriksi
literaalin määrittelemän vertailufunktion.
Tarkemmin sanoen: välitämme filter
-metodille
sulkeuman, jonka koodi on _ > raja
ja jolla
on pääsy tarvitsemaansa raja
-muuttujaan.
Tällainen ympäröivässä kontekstissa määritellyn muuttujan käyttö funktioliteraalissa (ja sulkeuman automaattinen muodostuminen literaalin perusteella) onkin aivan tavallista Scala-ohjelmissa.
Sulkeumista opit lisää jatkokurssilla tai omaehtoisella tiedonhaulla. Tämän kurssin puitteissa riittää tietää se, että äskeisen esimerkin kaltaista koodia voi mainiosti Scala-ohjelmaan kirjoittaa. Voit luonnolliseen tapaan käyttää kutsuvan koodin määrittelemää muuttujaa osana parametriksi välitettävän funktion ohjelmakoodia. Sulkeumille löytyy hyötykäyttöä tulevissa tehtävissä.
Lisää sisäkkäisyyttä
Paitsi että Scalassa voi määritellä funktioita sisäkkäin, niin luokat, yksittäisoliot ja funktiot voivat muutenkin olla eri tavoin sisäkkäin. Voit esimerkiksi määritellä luokan toisen sisälle tai yksittäisolion metodin sisälle.
Bonusaihe: funktio toisen paluuarvona
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 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 summaajatuotaSummaajafunktio(lisays: Int): Int => Int val kympinLisaaja = tuotaSummaajafunktio(10)kympinLisaaja: Int => Int = $$Lambda$5328/648803586@1347ef1b kympinLisaaja(5)res27: Int = 15 kympinLisaaja(6)res28: Int = 16 tuotaSummaajafunktio(100)(5)res29: Int = 105
Palautettavan funktion voi määritellä literaalillakin:
def tuotaSummaajafunktio(lisays: Int): Int => Int = _ + lisaystuotaSummaajafunktio(lisays: Int): Int => Int
Vapaaehtoinen lisäharjoitus: kuvan sumentaminen
Toteuta blur
-funktio
Vapaaehtoisena lisätreeninä tämän ja edellisen luvun aiheista
voit tehdä task10.scala
sta löytyvän tehtävän, jossa laadit
kuvaa sumentavan suotimen.
Vinkkejä:
Pic
-olioilla on metodeita, jotka kätevöittävät hommaa merkittävästi. Katso luokan dokumentaatiosta erityisesti metodittransformXY
japixelsNear
.Ehkäpä haluat määritellä
blur
-funktion sisään nimetyn apufunktion? Voisit esimerkiksi määritelläblurPixel
-nimisen funktion, joka määrittää sumennetun väriarvon annetulle koordinaattiparille. (Vrt. funktio toisen sisällä äskeisessäetsiIsot
-esimerkissä.)Laatisitko toisenkin apufunktion keskiarvon laskemiseen?
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Yhteenvetoa
Käteviin kokoelmia läpi käyviin metodeihin lukeutuvat edellisessä luvussa opittujen lisäksi myös erittäin yleiskäyttöiset
foldLeft
jareduceLeft
. Niillä voi muodostaa tulosarvon kokoelman alkioita vuoron perään työstämällä.Yhteen kiedottujen silmukka- ja
if
-rakenteiden sijaan on usein mielekkäämpää muotoilla algoritmi ketjuksi korkeamman asteen metodikutsuja, jossa yhden metodin paluuarvoon kohdistetaan seuraava operaatio vaiheittain.Funktion voi määritellä toisen sisään paikalliseksi funktioksi samaan tapaan kuin muuttujankin voi.
Funktiota määritellessä voi sen ohjelmakoodissa käyttää funktion itsensä ulkopuolisia muuttujia ympäröivästä kontekstista; näin syntyvää rakennetta kutsutaan sulkeumaksi eli klosuuriksi.
Lukuun liittyviä termejä sanastosivulla: korkeamman asteen funktio; kokoelma; abstraktiotaso; paikallinen funktio, käyttöalue; sulkeuma.
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
Sademäärätehtävä on muunnelma Elliot Solowayn klassikkotehtävästä.
Ensimmäiseen kirjoitetaan alkuarvo, joka on samalla lopputulos siinä tapauksessa, ettei kokoelmassa olisi alkioita lainkaan.