Luku 6.1: Funktioita parametreina
Johdanto
Tähän mennessä on tullut selväksi, että ohjelmiin liittyy:
dataa — esimerkiksi lukuja, tekstiä ja muita olioita — jota voi tallentaa muistiin ja johon voi kohdistaa:
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
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ä:
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.AuctionHouse
-olioilla on metodi, jolla voi pyytää luettelon kaikista sellaisista esineistä, joiden kuvauksessa esiintyy tietty tekstinpätkä.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.
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ä Color
in 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 green
in 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
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
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
Ä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.
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ä
tabulate
lle 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))
tabulate
n 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 tabulate
a. 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))
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 tabulate
lla 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.
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
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)
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.
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ä esimerkiksiprintCube
japrintIfPositive
ovat sopivia parametrejarepeatForEachElement
-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 turnElementsIntoResults
ille parametriksi
addToSum
-funktio, joka esiintyy myös tehtävän ensimmäisessä vaiheessa.)
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 turnElementsIntoResult
in 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 def
fata 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.
seuraava
-funktio yksinkertaisesti palauttaa parametriarvoaan yhtä isomman kokonaisluvun.