Luku 6.4: Lisää kokoelmia, lisää ohjelmia
Tästä sivusta:
Pääkysymyksiä: Olisiko lisää käteviä kokoelmametodeita? Miten yhdistelen kokoelmien metodeita?
Mitä käsitellään? Lisää kokoelmien metodeita ja uusia käyttötapauksia edellisen luvun esittelemille. Luvun lopussa puhutaan paikallisista funktioista sekä sulkeumista.
Mitä tehdään? Enimmäkseen ohjelmoidaan.
Suuntaa antava työläysarvio:? Kolme, neljä tuntia.
Pistearvo: A95 + B30 + C40.
Oheismoduulit: Election, FlappyBug, HigherOrder.
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
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 laatimisesta, 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
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.)Boolean
-tyyppinen. Tämä välitulos kertoo, onko negatiivista
lukua jo löydetty.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, glider.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. Sana left liittyy siihen, missä
järjestyksessä metodi käy alkiot läpi: foldLeft
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 palautusarvo 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) 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 10.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 teit taannoin kuten luvussa 5.6 ehdotettiin ja laadit totalVotes
eille avuksi
countVotes
-metodin, niin kohdista muutos siihen.
District
-luokasta edelleen puuttumaan jäävät metodit toteutetaan aikanaan luvussa 9.2.
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 voit 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 palautusarvo
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ää kurssin pisteytettyjen tehtävien puitteissa, 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:
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
Option
iin käärittynä, Int
-tyyppisenä.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
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. 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, 0, 0, 2, 1, 20, 1, 0, 1, 999999)exampleData: Vector[Int] = Vector(10, 0, 3, 999999, 2, -10, 0, 0, 2, 1, 20, 1, 0, 1, 999999) drySpell(exampleData, 3)res23: Int = 6
Ohjeita ja vinkkejä
Käytä jo esiteltyjä kokoelmametodeita. Opettele lisäksi nämä ja hyödynnä:
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ä:kokeilu.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 kerran(!) läpi. Iteraattorille voi kutsua monia tuttuja kokoelmametodeita kutenforeach
,toVector
jaindexWhere
. Jos haluat käsitellä kokoelman pätkiä usealla eri metodilla, voit kopioida pätkät esim. vektoriin. (Tässä tehtävässä se ei tosin ole välttämätöntä.)
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
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 )
filter
-metodille parametriksi
literaalin määrittelemän vertailufunktion.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 luokkia, yksittäisolioita ja funktioita voi muutenkin laittaa joustavasti eri tavoin sisäkkäin. Voit esimerkiksi määritellä luokan toisen sisälle tai yksittäisolion metodin sisälle.
Bonusaihe: funktio toisen palautusarvona
Luku 6.1 mainitsi, että korkeamman asteen funktioiksi sanotaan "funktioita, jotka käsittelevät funktioita" eli sellaisia funktioita, jotka joko ottavat parametreiksi funktioita tai palauttavat funktioita. Kaikki materiaalissa esiintyneet korkeamman asteen funktiot ovat kuitenkin nimenomaan ottaneet parametreiksi funktioita.
Kurssin tehtävissä emme kirjoita funktioita, jotka palauttavat funktioita, mutta tässä kuitenkin yksinkertainen esimerkki siitä:
def tuotaSummaajafunktio(lisays: Int): Int => Int = { def summaaja(n: Int) = n + lisays summaaja }tuotaSummaajafunktio: (lisays: Int)Int => Int val kympinLisaaja = tuotaSummaajafunktio(10)kympinLisaaja: Int => Int = $$Lambda$5328/648803586@1347ef1b kympinLisaaja(5)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
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 palautusarvoon 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, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä ja Aleksi Vartiainen.
Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.
Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista 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 ja Juha Sorva. 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. Pääkehittäjänä on tällä hetkellä Markku Riekkinen, jonka lisäksi A+:aa 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 ovat luoneet Nikolai Denissov, Olli Kiljunen ja Nikolas Drosdek yhteistyössä Juha Sorvan, Otto Seppälän, Arto Hellaksen ja muiden kanssa.
Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.
foldLeft
-metodilla on kaksi parametriluetteloa (kutentabulate
-metodilla; luku 6.1). Ensimmäiseen kirjoitetaan alkuarvo, joka on samalla lopputulos siinä tapauksessa, ettei kokoelmassa olisi alkioita lainkaan, ja...