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

Kurssin viimeisimmän version löydät täältä: O1: 2024

Luku 7.1: 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.

../_images/person04.png

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):

Ensimmäiseen kirjoitetaan alkuarvo, joka on samalla lopputulos siinä tapauksessa, ettei kokoelmassa olisi alkioita lainkaan.

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

Nämä viisi kohtaa sijoittuvat tasolle C. Niiden tekeminen ei ole kurssin jatkon kannalta ratkaisevaa, mutta voi olla opettavaista.

Tässä ja kahdessa seuraavassa kohdassa oletetaan, että numbers-muuttuja on tyyppiä Vector[Int] ja viittaa vektoriin, jossa on järjestyksessä kokonaisluvut 1, 2 ja 3.

Tarkastele seuraavaa lauseketta. Mikä sen arvo on? Huomioi myös merkkijonojen sisältämät välilyönnit.

numbers.foldLeft("Luvut: ")( (result, next) => result + next + " " )

Mikä seuraavista on tämän lausekkeen arvo?

Tässä ja seuraavissa kysymyksissä vaihtoehtoihin on kirjoitettu lainausmerkit ympärille, jotta välilyönnit erottuisivat paremmin. Lainausmerkit eivät sisälly lausekkeen arvona olevaan merkkijonoon.

Entä tämän lausekkeen arvo?

numbers.foldLeft("Luvut: ")( _ + _ + " " )

Tässä vielä yksi:

numbers.foldLeft("Luvut: ")( (result, next) => result + " " + next )

Tässä kohdassa oletetaan, että data on muuttuja, joka viittaa johonkin alkiokokoelmaan. Lisäksi oletetaan, että f on Int => Boolean -tyyppinen funktio.

foldLeft-käskyllä voi toteuttaa paljon erilaisia kokoelmiin liittyviä toimintoja. Toteutetaan tästä esimerkkinä forall-metodia vastaava toiminnallisuus — päteekö tietty ehto jokaiselle alkiolle? — käyttäen foldLeftiä. Tässä itu:

data.foldLeft(AAA)( (result, next) => result BBB f(next) )

Mitä kohtiin AAA ja BBB pitäisi kirjoittaa, jotta lauseke tuottaisi saman arvon kuin data.forall(f)?

Kirjoita edellisen kohdan lauseke käyttäen lyhennettyä funktioliteraalia nimettyjen parametrimuuttujien sijaan. (Korvaa tässäkin AAA ja BBB oikein.)

reduceLeft-metodi

Metodi reduceLeft muistuttaa foldLeftiä kovasti. Eroina ovat:

  • reduceLeftille 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.

  • reduceLeftin 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 reduceLeftiä 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 ifiä, Optionia ja reduceLeftiä 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 foldLeftiä tai map- ja sum-metodeita. Tai mieluiten mieti vaikka molemmat toteutustavat.

Jos taannoin laadit totalVoteseille 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.

  1. timePasses luokasta Game. Käytä foreach-metodia (luku 6.3).

  2. isLost samasta luokasta. Toteutus yksinkertaistuu huomattavasti luvun 6.3 esittelemällä metodilla, jolla voi nimensä mukaisesti suoraan tutkia, onko ehdon toteuttavaa estettä olemassa.

  3. Käyttöliittymän makePic-metodi. Korvaa silmukka foldLeft-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 makePiciin foldLeftiä, 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 foldLeftillä. (Kokeile, jos haluat.)

Itse asiassa kirjaston ohjelmakoodissa placeCopies on toteutettu nimenomaan foldLeftillä, joten epäsuorasti käytit sitä joka tapauksessa.

Jälkihuomio 2: jatkoa tehokkuuspohdinnoille

Käytit toivottavasti isLost-metodissa exists-metodia (tai kenties forallia).

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

Oletetaan, että data-niminen muuttuja viittaa johonkin kokoelmaan. Seuraava koodi käsittelee tätä kokoelmaa silmukan, if-käskyjen ja muuttujien avulla ja raportoi erään tuloksen. Koodissa käytetään apuna funktioita p, q ja r, jotka voivat olla mitä tahansa tyypiltään sopivia vaikutuksettomia funktioita.

var result = false
for element <- data do
  if p(element) then
    val temp = q(element)
    if r(temp) then
      result = true
    end if
  end if
end for
println(result)

Tarkastele nyt seuraavaa funktionaalisella tyylillä kirjoitettua koodia, josta puuttuu muutama ratkaisevan tärkeä kohta:

println(data.AAA(p).BBB(q).CCC(r))

Mitä metodeita pitäisi kutsua kohdissa AAA, BBB ja CCC, jotta tämä (huomattavasti lyhyempi ja yksinkertaisempi!) koodinpätkä tekisi saman asian kuin ylempänä oleva? Keksitkö itse jonkin käytännöllisemmän esimerkin, jossa tällaista koodinpätkää voisi soveltaa?

AAA pitäisi korvata tällä:

BBB pitäisi korvata tällä:

CCC pitäisi korvata tällä:

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 Optioniin 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 perusteella Int-vektori ja kutsu jo tekemääsi averageRainfallia.

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

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 slidingin 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ä.)

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ä

Tutki näitä sinänsä merkityksettömiä esimerkkifunktioita:

def etsiIsot(vektori: Vector[Int]) =
  def lasketaanIsoksi(luku: Int) = luku > 1000
  vektori.filter(lasketaanIsoksi)
def juttele() =
  import scala.io.StdIn.*
  def kysyNimi() =
    readLine("Anna nimi: ")
  def tervehdys(kohde: String) = "Ave, " + kohde
  val eka = kysyNimi()
  val toka = kysyNimi()
  println(tervehdys(eka + " ja " + toka))

Näiden kahden funktion sisään on kirjoitettu...

... def-sanalla toisia funktioita.

Mikä seuraavista väittämistä kuvaa näitä funktiomäärittelyjä?

Alla on pari koodinpätkää arvioitaviksi. Niissä yritetään käyttää yllä määriteltyjä funktioita.

// Yritys 1:
println(lasketaanIsoksi(5000))
// Yritys 2:
val isot = etsiIsot(Vector(2000, 300, 5000))
println(lasketaanIsoksi(5000))

Mikä seuraavista kuvaa näitä kahta esimerkkiä?

Pohdinta-aihe 2: paikalliset muuttujat

Tässä on hieman toisenlainen versio etsiIsot-funktiosta.

def etsiIsot(vektori: Vector[Int], raja: Int) =
  def lasketaanIsoksi(luku: Int) = luku > raja
  vektori.filter(lasketaanIsoksi)

Tämä funktio ottaa parametriksi rajan, jota suuremmat luvut pitäisi laskea isoiksi ja palauttaa osana tulosvektoria.

Paikallisessa funktiossa käytetään paitsi sen omaa muuttujaa luku...

... myös ympäröivän funktion muuttujaa raja.

Kysymys kuuluu: voiko noin tehdä? Voiko paikallinen funktio käyttää ympäröivässä funktiossa määriteltyjä nimiä kuten raja?

Entäpä toinen seikka samassa ohjelmassa?

def etsiIsot(vektori: Vector[Int], raja: Int) =
  def lasketaanIsoksi(luku: Int) = luku > raja
  vektori.filter(lasketaanIsoksi)

Funktiomme kutsuu vektorin filter-metodia. Vector-luokka metodeineen on määritelty ihan toisaalla. filter-metodin koodi suoritetaan omassa kehyksessään kutsupinossa.

Välitämme filter-metodilla parametriksi funktion, joka käyttää raja-muuttujaa, joka on määritelty ympäröivän funktion paikalliseksi muuttujaksi. filter-metodia ei kuitenkaan ole määritelty etsiIsot-funktion sisällä.

Toimiiko moinen?

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ä. filterhä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.scalasta 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 metodit transformXY ja pixelsNear.

  • 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 ja reduceLeft. 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, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, 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 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 nyt 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 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ä.

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