Kurssin viimeisimmän version löydät täältä: O1: 2024
Luku 5.5: Funktioita parametreina
Tästä sivusta:
Pääkysymyksiä: Miten funktiot voivat käsitellä funktioita? Miten välitän funktiolle parametriksi funktion? Miksi on hienoa, että voin tehdä niin?
Mitä käsitellään? Pääaiheena ovat korkeamman asteen funktiot, erityisesti funktioparametrit. Lisäksi käsitellään usean parametriluettelon kirjaamista Scala-funktiolle, uusia menetelmiä kokoelmien luomiseen sekä sisäkkäisiä eli "moniulotteisia" kokoelmia. Paljon kuvankäsittelyesimerkkejä.
Mitä tehdään? Luetaan ja ohjelmoidaan. Useita pieniä tehtäviä.
Suuntaa antava työläysarvio:? Kolme ja puoli tuntia.
Pistearvo: A50 + B25 + C15.
Oheisprojektit: HigherOrder (uusi). AuctionHouse1-projektikin käy ohimennen esimerkkinä.
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 palautusarvoina.
Ennen kuin käsittelemme sitä, mitä hyötyä tällaisesta voi olla, katsotaan konkreettinen esimerkki.
Funktion välittäminen parametrina
Lyhyen tähtäimen suunnitelma:
- Määritellään kaksi pientä funktiota —
seuraava
jatuplaa
— jotka kuvaavat sellaisia toimenpiteitä, jotka voi kohdistaa kokonaislukuun ja jotka tuottavat kokonaislukutuloksen. - Määritellään funktio nimeltä
kahdesti
, jolla voi suorittaa minkä tahansa tuollaisen kokonaislukufunktion kahteen kertaan. Tieto siitä, mikä funktio suoritetaan kahteen kertaan, välitetäänkahdesti
-funktiolle parametriksi.
Aloitetaan kohdasta yksi ja määritellään aluksi pari ihan tavallista, irrallista funktiota. Ensin tämä:
def seuraava(luku: Int) = luku + 1seuraava: (luku: Int)Int seuraava(100)res0: Int = 101
seuraava
-nimi viittaa funktioon,
joka ottaa parametriksi Int
arvon...Int
-arvon. Voidaan sanoa, että
seuraava
-funktio on tyyppiä Int => Int
, missä nuolen
vasemmalla puolella on parametrin tyyppi ja oikealla
palautusarvon tyyppi.tuplaa
-funktio on sekin tyyppiä Int => Int
:
def tuplaa(tuplattava: Int) = 2 * tuplattavatuplaa: (tuplattava: Int)Int tuplaa(100)res1: Int = 200
kahdesti
-funktio
kahdesti
-funktion tehtävänä on suorittaa parametriksi annettu toimenpide kahteen
kertaan. Haluttaisiin, että sitä voisi käyttää näin:
kahdesti(seuraava, 1000)res2: 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)res3: Int = 4000
Määritellään kahdesti
-funktio näin:
def kahdesti(toiminto: Int => Int, kohde: Int) = toiminto(toiminto(kohde))kahdesti: (toiminto: Int => Int, kohde: Int)Int
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 toimii siten, että kutsutaan ensimmäiseksi
parametriksi saatua funktiota toiseksi parametriksi välitetylle
kohdeluvulle ensin kerran ja sitten saadulle palautusarvolle
uudestaan.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.kahdesti
viittaa funktioon, joka...- ottaa toiseksi parametrikseen kokonaisluvun.
- ja palauttaa kokonaisluvun.
Väliajatus
Vertaile keskenään:
- Funktio voidaan 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 palautusarvoina, 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. Kuitenkin monissa kielissä voi myös laatia korkeamman asteen funktioita. Scalassakin tämä on mahdollista, kuten jo näitkin.
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 palautusarvoina 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ä:
- 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.
- 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.
- 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.
- 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)res4: Boolean = true onkoJarjestyksessa("Haskell", "Java", "Scala", vertaaPituuksia)res5: Boolean = false onkoJarjestyksessa("Java", "Scala", "Haskell", vertaaMerkkeja)res6: Boolean = false onkoJarjestyksessa("Haskell", "Java", "Scala", vertaaMerkkeja)res7: Boolean = true onkoJarjestyksessa("200", "123", "1000", vertaaIntArvoja)res8: Boolean = false onkoJarjestyksessa("200", "123", "1000", vertaaPituuksia)res9: 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".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.3 AuctionHouse
-luokkaa. Otetaan pari lisätavoitetta.
Halutaan, että:
AuctionHouse
-olioilla on metodi, jolla voi pyytää luettelon kaikista sellaisista myyntiin laitetuista 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 myyntiin laitetuista 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 {
private val items = Buffer[EnglishAuction]()
// ... muita metodeita ...
def findAll(checkCriterion: EnglishAuction => Boolean) = {
val found = Buffer[EnglishAuction]()
for (currentItem <- this.items) {
if (checkCriterion(currentItem)) {
found += currentItem
}
}
found.toVector
}
}
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.if
-käskyn sisällä tarkastettaessa,
täyttyykö hakukriteeri.Nyt luokkaa voi käyttää vaikkapa näin:
object FindAllTest extends App {
def checkIfOpen(candidate: EnglishAuction) = candidate.isOpen
def checkIfHandbag(candidate: EnglishAuction) = candidate.description.toLowerCase.contains("handbag")
val house = new AuctionHouse("ReBay")
house.addItem(new EnglishAuction("A glorious handbag", 100, 14))
house.addItem(new 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.2 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)
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()
transformPic
-metodille annetaan parametriksi jokin
Color => Color
-tyyppinen funktio, tässä swapGreenAndBlue
.
Se soveltaa tuota funktiota kuhunkin pikseliin ja palauttaa
näin muodostamansa uuden kuvan.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 projektin 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 oleva?
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. Kuitenkin 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()
Color.Max
on kunkin RGB-komponentin erilaisten arvojen
määrä, tässä käytännössä 256, koska arvot ovat välillä 0–255.)Pic.generate
(joka on Pic
-kumppaniolion metodikutsu;
luku 5.1) 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) Red else if (x + y < 200) Black else if (y % 10 < 5) 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). Hyvin 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: Int) = eka * toka + kolmas * neljaskokeilu: (eka: Int, toka: Int)(kolmas: Int, neljas: Int)Int
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, 4)res10: Int = 14 kokeilu(1, 2, 3, 4)<console>:9: error: too many arguments for method kokeilu: (eka: Int, toka: Int)(kolmas: Int, neljas: Int)Int
On olemassa tapauksia, joissa usean parametriluettelon käyttö on kätevää. Aihetta ei kuitenkaan tällä kurssilla juurikaan käsitellä, eikä sinun tarvitse kurssin puitteissa 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 tilanne on seuraavassa esimerkissä.
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)res11: 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.tabulate
kutsuu parametrina saamaansa
funktiota (tässä tuplaa
) kullekin indeksille (tässä luvuille 0–9).Tässä vastaava esimerkki seuraava
-funktiolla:
Buffer.tabulate(5)(seuraava)res12: 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.Boolean
-arvoja
sisältävä vektori.mkString
-metodilla. Erottimena on tässä käytetty
sarkain- eli tabulaattorimerkkiä, joka merkitään merkkijonoon
\t
(luku 4.5).Vektorillinen kasvavahkoja satunnaislukuja, ole hyvä:
import scala.util.Randomimport scala.util.Random
def 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
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 metodille on laitettu 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
"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.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ä. Yleensä sisäkkäinen ratkaisu on kuitenkin kätevämpi.
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) { 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
vektorillinenRiveja
on Vector[Vector[Int]]
, jonka
alkiotkin ovat vektoreita, niin...luvutRivilla
tyyppi on
Vector[Int]
.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)
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)) 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, jos laitat kirkkausrajaksi jotakin muuta 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 aiemmista luvuista merkittävästi 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-projektista.
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 lopussa olevat 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. Funktion tulee muokata annettua puskuria siten, että se käyttää parametrina saamaansa muunnosfunktiota korvatakseen kunkin alkion uudella.
(Ajatus on siis samansukuinen kuin kuvien transformColor
-metodissa edellä, mutta tässä
laadittava funktio muokkaa alkuperäistä kokoelmaa, ei tuota uutta.)
Löydät tarkemman tehtävänannon tiedostosta Task8.scala
.
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.
Vinkki: Käytä for
-silmukkaa sekä kokoojamuuttujaa, jolla pidät kirjaa tuloksesta.
Aluksi kannattaa myös katsoa seuraava graafinen esitys, joka kuvailee, miten
turnElementsIntoResult
-funktion pitäisi toimia.
Kun summan laskeminen turnElementsIntoResult
in avulla onnistuu, siirry tehtävän
toiseen vaiheeseen. Suunnittele ja toteuta funktiot positiveCount
ja productOfNonZeros
ja käytä niitä positiivisten lukujen laskemiseen ja alkioiden tulon muodostamiseen.
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 API:ssa. Aivan pian luvussa 6.2 näet, että Scalan valmiilla kokoelmatyypeillä on koko liuta korkeamman asteen metodeita, joilla kokoelmien sisältöä voi käsitellä erittäin 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 voi olla epäkäytännöllistä. Onneksi helpotusta on tiedossa heti seuraavassa luvussa 6.1, 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ä.
- Alkiokokoelmia voi laittaa 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!
Kierrokset 1–13 ja niihin liittyvät tehtävät ja viikkokoosteet on laatinut Juha Sorva.
Kierrokset 14–20 on laatinut Otto Seppälä. Ne eivät ole julki syksyllä, mutta julkaistaan ennen kuin määräajat lähestyvät.
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 Riku Autio, Jaakko Kantojärvi, Teemu Lehtinen, 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 ovat suunnitelleet Juha Sorva ja Teemu Sirkiä. Niiden teknisen toteutuksen ovat tehneet Teemu Sirkiä ja Riku Autio käyttäen Teemun toteuttamia Jsvee- ja Kelmu-työkaluja.
Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset on laatinut Juha Sorva.
O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.
Opetustapa, jossa 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+ on luotu Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Pääkehittäjänä toimii tällä hetkellä Jaakko Kantojärvi, jonka lisäksi järjestelmää kehittävät useat tietotekniikan ja informaatioverkostojen opiskelijat.
Kurssin tämänhetkinen henkilökunta on kerrottu luvussa 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.