Kurssin viimeisimmän version löydät täältä: O1: 2024
Luku 1.7: Omien funktioiden luominen
Tästä sivusta:
Pääkysymyksiä: Miten funktio käyttää sille välitettyjä parametreja, ja miten se muutenkin saa tehtävänsä hoidettua? Miten kirjoitan itse Scala-funktion, joka hoitaa jonkin tehtävän?
Mitä käsitellään? Funktioiden määritteleminen koodiksi: def
,
parametrimuuttujat, funktion runko, arvon palauttaminen, paikalliset
muuttujat. Funktion suorituksen vaiheet; kutsupino ja kehykset.
Mitä tehdään? Luetaan ja tehdään harjoituksia. Ohjelmoidaan itse REPLin ulkopuolellakin.
Suuntaa antava työläysarvio:? Pari tuntia tai yli. Monille tähän mennessä hankalin luku. Ensiaskelet koodin kirjoittamisen parissa voivat olla haparoivia, vaikka tässä ei vielä mitään erityisen monimutkaista tehdäkään.
Pistearvo: A90.
Oheismoduulit: Aliohjelmia.
Muuta: Eräissä kohdissa on kaiuttimista tai kuulokkeista hyötyä. Aivan pakolliset ne eivät ole.
Johdanto
Tässä luvussa käsittelemme osin samoja funktioita kuin edellisessä ja osin uusia, mutta nyt katsomme myös funktiot toteuttavaa ohjelmakoodia.
Käyttääksesi tämän luvun funktioita itse REPLissä, käynnistä REPL kuten edellisessä luvussa: varmista, että Project-näkymässä on valittuna Aliohjelmia-moduuli, kun painat Ctrl+Shift+D. (Vaihtoehtoisesti sekin toimii, että avaat Aliohjelmia-moduulin sisältämän kooditiedoston aktiiviseksi editoriin ja käynnistät sitten REPLin.)
Löydät lukuun liittyvän esimerkkikoodin moduulin sisältä tiedostosta
o1/aliohjelmia/esimerkit_luku17.scala
.
Funktion määrittely
Tarkastellaan luvusta 1.6 tuttua keskiarvo
-funktiota, jota käytetään REPLissä näin:
keskiarvo(2.0, 5.5)res0: Double = 3.75
Jotta tuo käsky toimisi, pitää keskiarvofunktion ensin olla määritelty. Tämän funktion määrittely on varsin yksinkertainen, mutta siinä tulee jo esiin useita tärkeitä asioita.
Funktion toimintaperiaatteen voi kuvata suomeksi näin: "Kun keskiarvo
-funktiota
kutsutaan, se muodostaa paluuarvon laskemalla ensimmäisen parametrin arvo yhteen
toisen parametrin arvon kanssa ja jakamalla summan kahdella". Alla on koodinpätkä, joka
määrittelee keskiarvofunktion juuri tähän tapaan mutta Scala-kielellä.
(Tästä eteenpäin esimerkit vähitellen monimutkaistuvat, ja monet niistä näytetään seuraavanlaisissa laatikoissa, joissa erilaiset ohjelman osat on korostettu värein. IntelliJ’hän tekee Scala-koodille samoin, joskin eri väreillä.)
def keskiarvo(eka: Double, toka: Double) = (eka + toka) / 2
Tässä ilmoitamme, että "silloin kun keskiarvo
-funktiota
kutsutaan, pitää antaa parametreiksi kaksi Double
-arvoa".
Yleisemmin sanoen: tässä määritellään funktion parametrimuuttujat
(virallisemmin muodolliset parametrit; formal parameters)
ja niiden tietotyypit.
Parametrimuuttujille määritellään nimet. Tässä ensimmäisen
parametrimuuttujan nimi on yksinkertaisesti eka
ja toisen toka
.
Huomaa yhtäsuuruusmerkki, jonka perään kirjoitetaan...
... funktion varsinainen toteutus eli funktion runko
(function body). Funktion runko toteuttaa sen algoritmin,
jolla funktion tehtävä saadaan hoidettua. Tässä tapauksessa
runkona on yksi aritmeettinen lauseke, joka määrää, minkä arvon
keskiarvo
-funktio palauttaa kutsuttaessa.
Huomaa, että parametrimuuttujat eka
ja toka
todella ovat muuttujia, tarkemmin
sanoen val
-muuttujia. Parametrimuuttujat eivät saa arvojaan muiden muuttujien lailla
sijoituskäskyllä vaan funktiokutsusta, mutta muuten niitä voi käyttää aivan tuttuun tapaan.
Funktiokutsun vaiheet ja kutsupino
Seuraavassa animaatiossa näet jälleen vaiheittain muuttuvan diagrammin tietokoneen muistiin tallennetuista tiedoista. Tällä kertaa katsomme funktion suorituksen sisään, ja näet esimerkiksi parametrimuuttujien toimintaperiaatteen paremmin. Katso tämä animaatio teksteineen perusteellisesti! Se esittelee jatkon kannalta erittäin tärkeitä uusia käsitteitä.
Animaation kuvittama pieni ohjelmanpätkä ei tee mitään mullistavaa, kunhan vähän koekäyttää keskiarvofunktiota:
Animaatiossa näit kehyksiä (frame). Kehykset muodostavat kutsupinon (call stack tai vain stack), joka todella toimii kuin pino: kun kutsu käynnistyy, lisätään kutsupinoon uusi kehys, ja kun kutsu päättyy arvon palauttamiseen, poistetaan tuo kehys pinon päältä. Tietokone siis pitää kutsupinon kehyksissä kirjaa siitä, mitä funktiokutsuja on kullakin hetkellä ohjelman suorituksen aikana käynnissä ja mitä tietoja näihin kutsuihin liittyy. Aktiivisena on aina pinon kulloinkin päällimmäinen kehys, kun taas alemmat kehykset (joita äskeisessä esimerkissä oli vain yksi) odottavat, että niiden käyttö voi jatkua, kunhan viimeksi kutsutun funktion suoritus on päättynyt.
Toivottavasti animaatio jo selkeytti funktiokutsujen vaiheita. Joka tapauksessa kutsupinon ja kehysten merkitys korostuu pian, kunhan käsittelemme vähän monimutkaisempia funktioita.
Funktiokutsu — siis mikä?
Luvussa 1.2 mainittiin, että sanalla "ohjelma" viitataan joskus staattiseen ohjelmakoodiin ("Ohjelmassa on 100 riviä.") ja joskus siihen dynaamiseen prosessiin, joka syntyy, kun ohjelma ajetaan ("Ohjelma tallensi käyttäjän syöttämät tiedot.").
"Funktiokutsu" on moniselitteinen samalla tavoin. Joskus sillä tarkoitetaan ohjelmakoodiin kirjoitettua lauseketta: "Rivin 15 funktiokutsussa on välimerkkivirhe." Joskus taas viitataan siihen prosessiin, jota äskeinen animaatio esitteli ja joka syntyy funktiota suoritettaessa ohjelma-ajon aikana: "Funktiokutsu päättyi, ja palautettiin arvo 4."
Molemmista merkityksistä on hyvä tietää, kun luet kurssimateriaalia ja muuta ohjelmointiin liittyvää.
Funktionmäärittelemisharjoitus
Ohjelmointitehtävä: metreiksi
Tehtävänanto
Isossa-Britanniassa ja joissakin sen entisissä siirtomaissa käytetään edelleen laajasti mittayksikköinä tuumaa ja jalkaa. Yksi tuuma on 2,54 cm, ja yksi jalka on 12 tuumaa. Mittoja on tapana ilmaista näitä yksiköitä yhdistelemällä. Esimerkiksi noin 180-senttisen ihmisen pituuden voi sanoa olevan 5 jalkaa ja 11 tuumaa.
Toteuta vaikutukseton Scala-funktio, joka
on nimeltään
metreiksi
,ottaa ensimmäiseksi parametriksi jalkojen lukumäärän
Double
-arvona,ottaa toiseksi parametriksi tuumien lukumäärän samaan tapaan, ja
laskee ja palauttaa
Double
-arvon, joka kertoo montako metriä annetut jalat ja tuumat ovat yhteensä (esim. parametriarvojen ollessa 5 ja 11 palautetaan 1.8034).
Etene seuraavissa vaiheissa: pohjustus, koodin kirjoittaminen, testaus. Tarvittaessa korjaa ja testaa uudestaan, kunnes funktio toimii. Alla on lisäohjeita kustakin vaiheesta.
Työn pohjustus
Ota IntelliJ’ssä esiin moduuli Aliohjelmia ja sieltä tiedosto o1/aliohjelmia/kierros1.scala
.
Huomaat tiedoston alkupäässä merkityn kohdan, johon tässä luvussa kirjoitetaan funktioita.
Koodin kirjoittaminen
Varmista ensin, että ymmärrät täsmälleen, mitä funktion on tarkoitus tehdä! Varo, ettei tämä periaatteessa itsestään selvä vaihe unohdu. Yksinkertaiset huolimattomuusvirheetkin ongelman kuvauksen lukemisessa voivat aiheuttaa kipuja.
Kirjoita funktion koodi merkittyyn paikkaan. Tämä tehtävä ratkeaa yhdellä koodirivillä.
Aloita kirjoittamalla
def
ja funktion nimi.Kirjoita parametrimäärittelyt. Nimeä parametrimuuttujat itse mielekkäällä tavalla.
Kirjoita funktion runko. Tässä se on yksi aritmeettinen lauseke, joka tekee halutun yksikkömuunnoksen. (Älä tulosta arvoa vaan palauta se!)
IntelliJ’n virheilmoituksista
Koodia editoidessasi huomaat, että silloin tällöin näkyviin ilmestyy punaisia merkintöjä, mm. sahalaitaisia alleviivauksia: . Myös osa koodista voi värjäytyä punaiseksi. Tämä on IntelliJ’n tapa ilmoittaa havaitsemistaan ongelmista.
Älä häkelly, vaikka näitä merkintöjä ilmestyisi heti, kun alat koodia kirjoittaa. IntelliJ nimittäin varoittelee koodin nykytilan perusteella, ja keskeneräinen koodi tuottaa helposti virheilmoituksia. Esimerkiksi siinä vaiheessa, kun olet kirjoittanut vasta
def
-sanan ja nimen, IntelliJ jo älähtää, koska funktiomäärittely on vielä puutteellinen.Nämä punaiset merkinnät ovat suuri apu ohjelmoijalle, ja niihin on kyllä syytä suhtautua vakavuudella, mutta vasta sitten, jos niitä on jäljellä, kun olet saanut
metreiksi
-funktion mielestäsi oikein kirjoitettua.Tarkempia virheilmoituksia voi katsoa jättämällä hiiren kursorin virheellisinä korostettujen kohtien päälle. Ikävä kyllä virheilmoitustekstien tulkinta voi olla tähän mennessä opitun perusteella vaikeaa; kurssimateriaalin esimerkeistä voi olla tässä vaiheessa enemmän apua virheiden korjaamisessa.
Huolehdi, ettei koodiin jää IntelliJ’n tekemiä punaisia virhemerkintöjä. Kitke erityisesti välimerkkivirheet. Käytitkö ainakin kaarisulkeita, pilkkua ja yhtäsuuruusmerkkiä? Ja pistettä desimaalierottimena? Kirjoititko muuttujien nimet kaikissa kohdissa juuri samalla tavalla?
Koodin kääntäminen
Ennen kun tietokone voi ajaa Scala-koodisi (REPLissä tai muutenkaan), sen on käännettävä (compile) tuo koodi muotoon, jonka se pystyy suorittamaan. Kääntäessä kone myös tarkistaa, että koko koodi noudattaa kaikkia kielen "pelisääntöjä". Samalla voi ilmetä virheitä, jotka pitää korjata ennen kuin koodia voi käyttää.
IntelliJ kääntää koodisi automaattisesti aina kun käynnistät jonkin ohjelman joko aivan ensimmäistä kertaa tai muokkausten jälkeen. Voit myös koska tahansa pyytää IntelliJ’tä kääntämään ohjelmasi varmistaaksesi, ettei siinä ole automaattisesti havaittavia virheitä. Kokeile tätä nyt.
Valitse IntelliJ’n valikosta Build → Build Module 'Aliohjelmia' tai kätevämmin vastaavalla pikanäppäimellä F10. (IntelliJ’ssä käännöksen käynnistystoiminnon nimenä on "build", joka viittaa yleisesti ohjelman käyttövalmisteluun.)
Pienen hetken jälkeen tapahtuu jompikumpi seuraavista.
Joko:
Esiin ei tule virheilmoituksia vaan vain melko huomaamaton teksti vasempaan alakulmaan: Build completed successfully. Jos näin käy, hyvä, mutta teepä silti nyt tahallasi jokin pieni välimerkkivirhe koodiisi ja paina uudestaan F10, niin näet toisenkin tapahtumainkulun.
Tai:
Esiin ponnahtaa Build-välilehti ja sinne yksi tai useampia virheilmoituksia. Tämä virheilmoitusluettelo on yleensä samankaltainen (mutta joissain tapauksissa täydellisempi) kuin editorin punaisella merkityt virheet. Koodissa on korjattavaa. Korjaa ja paina uudestaan F10.
Virheiden korjaamisesta ja kääntämisestä
Ennen kuin yrität ajaa ohjelmaasi, huolehdi aina ensin siitä, että olet poistanut IntelliJ’n värittämät virheet. Muuten seuraava vaihe eli testaaminen ei onnistu lainkaan!
IntelliJ on sen verran ovela, että se osaa näyttää useimmat ohjelma-ajon estävät virheet "etukäteen" editorissa. Osa tällaisista virheistä voi kuitenkin ilmetä vasta kun koko koodi käännetään.
Koska IntelliJ kääntää koodisi automaattisesti — esim. kun käynnistät REPLin — ei sinun välttämättä tarvitse erikseen käynnistää käännöstä. Pienellä vaivalla voit kuitenkin koska vain painaa F10 ja näin tarkistuttaa koodisi kieliopillisuuden.
Testaus
Käynnistä REPL tuttuun tapaan Ctrl+Shift+D huolehtien, että siihen latautuu juuri Aliohjelmia-moduuli. Huomaa, että testataksesi uutta koodia tarvitset uuden REPL-session.
Kokeile kutsua
metreiksi
-funktiotasi erilaisilla parametriarvoilla. Toimiiko se oikein? Jos ei, niin palaa yllä olevaan kappaleeseen Koodin kirjoittaminen (sen kohtaan yksi, ei suoraan kohtaan kaksi!). Pyydä tarvittaessa assistentilta apua.(P.S. Huomasitko, että piti palauttaa metrejä eikä senttimetrejä?)
REPLin "buuttaamisesta"
Aina, kun olet muuttanut kooditiedostoasi ja haluat testata sitä REPLissä, on uusi versio ladattava REPLiin. Voit joko sulkea REPLin ja käynnistää sen uudestaan, tai painaa REPLin vasemman yläkulman Rerun-kuvaketta . Jos haluat sitten antaa aiempia käskyjä uudestaan, löydät ne ylä- ja alanuolella kelailemalla.
Palauttaminen
Kun funktiosi toimii, ole iloinen ja palauta ratkaisusi IntelliJ’n kautta.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Vapaaehtoista lisätreeniä
Jos haluat, voit kehittää ohjelmointirutiiniasi tällä helpolla treenitehtävällä, joka muistuttaa edellistä tehtävää.
Laadi seuraavat funktiot samaan kierros1.scala
-tiedostoon:
Funktio
kuutionTilavuus
, jolle annetaan ainoaksi parametriksi kuution sivun pituus ja joka palauttaa tuonkokoisen kuution tilavuuden. Parametri ja paluuarvo ovatDouble
-tyyppisiä.Funktio
kuutionAla
, joka samaan tapaan laskee ja palauttaa annetun sivunmitan perusteella kuution pinta-alan.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Funktioiden määritteleminen REPLissä
Äskeisessä tehtävässä kirjoitit ja tallensit funktion erilliseen tiedostoon IntelliJ’n editorissa. Samoin tehdään tulevissa tehtävissä. On silti vaihtoehtoisesti mahdollista kirjoittaa funktioiden määrittelyjä suoraan REPLiin:
def keskiarvo(eka: Double, toka: Double) = (eka + toka) / 2keskiarvo(eka: Double, toka: Double): Double keskiarvo(2, 1)res2: Double = 1.5
Tämä toinen tapa on pikkukokeiluissa näppärä. IntelliJ’n editorin käyttö kuitenkin valmistaa paremmin tuleviin tehtäviin, ja se on tehtävien palauttamisen kannalta kätevämpää. Editorilla luomasi koodi jää myös itsellesi talteen.
Omissa kokeiluissasi voit hyvin määritellä funktioita myös REPLissä.
Funktionlaatimisharjoitusta ja modulo-operaattori
Johdanto modulo-operaattoriin
Tutuimpien aritmeettisten operaattorien lisäksi ohjelmoinnissa käytetään melko usein myös
modulo-operaattoria, joka merkitään Scalassa %
. Tätä operaattoria käytetään useimmiten
kokonaislukujen kanssa; voidaan esimerkiksi laskea lausekkeen 25 % 7
arvo.
Kokeile modulo-operaattorin käyttöä itse REPLissä erilaisilla arvoilla. Vastaa sitten seuraavaan pikkukysymykseen ja laadi muutama funktio, joissa sovellat tätä operaattoria.
Parillisuusesimerkki
Pannaan tietokone tutkimaan luvun parillisuutta. Parillisuuden tutkimisesta voi olla hyötyä vaikkapa silloin, jos halutaan kohdentaa jokin toimenpide joka toiseen kohteeseen, esimerkiksi värittää suuren taulukon parilliset rivit eri taustavärillä kuin parittomat. Tässä käytämme parillisuutta vain lisäesimerkkinä funktion laatimisesta ja modulo-operaattorista.
Nyt laadittavan vaikutuksettoman funktion olisi tarkoitus palauttaa nolla merkiksi siitä, että annettu luku on parillinen. Muutoin se palauttaa 1 tai -1 annetun luvun etumerkin mukaan. Siis näin:
parillisuus(100)res3: Int = 0 parillisuus(2)res4: Int = 0 parillisuus(103)res5: Int = 1 parillisuus(7)res6: Int = 1 parillisuus(-7)res7: Int = -1
Modulo-operaattoria käyttäen funktio on helppo toteuttaa: jaettaessa luku kahdella saadaan jakojäännökseksi nolla vain, jos luku oli parillinen. Tässä toteutus:
def parillisuus(tutkittava: Int) = tutkittava % 2
Voit määritellä tämän funktion itse ja kokeilla sitä REPLissä. Tai siirtyä suoraan seuraavaan tehtävään.
Shakkilautatehtävä: johdanto
Olkoon shakkilaudan ruudut numeroitu järjestyksessä 1–64 alkaen vaikkapa vasemmasta yläkulmasta, vasemmalta oikealle rivi kerrallaan. Lisäksi laudan rivit ja sarakkeet (eli shakkikielessä linjat) on kummatkin numeroitu 1–8. Oikealla on kuva.
Jos tiedossa on ruudun numero, vaikkapa 35, niin miten saamme tietokoneen määrittämään ruudun rivi- ja sarakenumerot? Yksi mahdollisuus on laatia kaksi vaikutuksetonta funktiota, joita voi sitten käyttää seuraavasti:
rivi(35)res8: Int = 5 sarake(35)res9: Int = 3
Funktiot voi määritellä näin:
def rivi(ruutu: Int) = ((ruutu - 1) / 8) + 1
def sarake(ruutu: Int) = ((ruutu - 1) % 8) + 1
Shakkilautatehtävä: tehtävänanto
Mitä jos ruudut olisikin numeroitu 0–63 ja rivit ja sarakkeet 0–7, kuten viereisessä
kuvassa? Laadi vaikutuksettomat funktiot rivi
ja sarake
, jotka toimivat tähän tapaan:
rivi(34)res10: Int = 4 sarake(34)res11: Int = 2
Ohjeita ja vinkkejä:
Toimi samaan tapaan kuin
metreiksi
-tehtävässä.Kirjoita nämäkin funktiot
kierros1.scala
-tiedostoon.Jos ymmärrät yllä annetut versiot, joissa numerointi alkaa ykkösestä, niin tehtävä ei ole vaikea. Ratkaisut ovat nollasta alkavalla numeroinnilla samansuuntaiset mutta yksinkertaisemmat.
Testaa REPLissä ennen kuin palautat.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Usea käsky funktiossa
Laaditaan kokeeksi funktio, jonka olisi tarkoitus tulostaa joka kutsukerralla kolme vakioriviä tekstiä sekä neljäntenä rivinä päätösrivi, joka annetaan funktiolle parametriksi. Funktiota voisi käyttää näin:
haukiOnKala("T. Kalatalouden Keskusliitto")Kun hauki on vähärasvainen, sitä voidaan säilyttää pakastettuna jopa 6 kuukautta. Vertailun vuoksi mainittakoon, että haukea rasvaisemman lahnan vastaava säilymisaika on vain puolet eli 3 kuukautta. T. Kalatalouden Keskusliitto haukiOnKala("Sen pituinen se, ja tosipa se lienee.")Kun hauki on vähärasvainen, sitä voidaan säilyttää pakastettuna jopa 6 kuukautta. Vertailun vuoksi mainittakoon, että haukea rasvaisemman lahnan vastaava säilymisaika on vain puolet eli 3 kuukautta. Sen pituinen se, ja tosipa se lienee.
Funktion määrittely on seuraava.
def haukiOnKala(loppukaneetti: String) =
println("Kun hauki on vähärasvainen, sitä voidaan säilyttää pakastettuna jopa 6 kuukautta.")
println("Vertailun vuoksi mainittakoon, että haukea rasvaisemman lahnan vastaava")
println("säilymisaika on vain puolet eli 3 kuukautta.")
println(loppukaneetti)
haukiOnKala
-funktion rungossa on useita käskyjä peräkkäin.
Tällöin koodirivit kirjoitetaan omille riveilleen tähän tapaan.
Tässä siis määritellään, että kaikki neljä println
-käskyä
suoritetaan peräjälkeen, kun haukiOnKala
-funktiota kutsutaan.
Tämä on ensimmäinen esimerkki, jossa näemme usealle riville jakautuvan sisäkkäisen rakenteen ohjelmakoodissa. Tällaiset rakenteet sisennetään (indent) kuten tässä.
Entä paluuarvo?
haukiOnKala
-funktiomme palauttaa sisällöttömän Unit
-arvon eli niinsanotusti "ei palauta
arvoa". Tämä johtuu siitä, että funktion palauttama arvo määräytyy sen mukaan, millaisen
arvon viimeisenä suoritettava funktion rungon käskyistä tuottaa. Tässä tapauksessa se on
viimeinen println
-käsky, joka tuottaa vain sisällöttömän Unit
-arvon (kuten luvussa 1.6
todettiin) . Samainen Unit
-arvo toimii nyt myös println
-funktiokutsun sisältävän
haukiOnKala
-funktion paluuarvona.
Rivitys ja sisennykset funktion koodissa
Äsken tuli esille, että funktion koodi jaetaan joskus usealle riville ja runko joskus sisennetään. Tämä on tärkeämpää kuin ehkä heti arvaisi. Katsotaanpa tätä vielä tarkemmin.
Sisennysten merkitys
Sisennyksiä käytetään ohjelmointikielissä laajasti. Joissakin kielissä sisennykset ovat täysin vapaaehtoiset eivätkä vaikuta ohjelman toimintaan, mutta sisentämistä pidetään silti hyvänä tyylinä, koska sisennykset korostavat ohjelman rakennetta ja helpottavat lukemista. Toisissa kielissä sisennyksillä myös aidosti vaikutetaan ohjelman rakenteeseen ja toimintaan.
Scala 3 kuuluu jälkimmäiseen ryhmään: sisennykset eivät ole pelkkä tyyliseikka. Edellinen funktiomme ei toimisi, jos sen kirjoittaisi vaikkapa näin sekavasti sisentäen:
// Ei toimi.
def haukiOnKala(loppukaneetti: String) =
println("Kun hauki on vähärasvainen, sitä voidaan säilyttää pakastettuna jopa 6 kuukautta.")
println("Vertailun vuoksi mainittakoon, että haukea rasvaisemman lahnan vastaava")
println("säilymisaika on vain puolet eli 3 kuukautta.")
println(loppukaneetti)
Ero kieliversioiden välillä
Äskeinen teksti pätee Scala-kielen nykyversioon 3, jota käytämme. Kielen aiemmissa versioissa sisennykset olivat vapaaehtoiset. Tällöin oli käytettävä ylimääräisiä sulkumerkkejä funktion rungon rajaamiseen, minkä lisäksi sisennykset oli kuitenkin tapana kirjoittaa.
Saatat törmätä vanhalla kieliversiolla kirjoitettuun koodin nettilähteissä tai vanhoissa kirjoissa. Vanhasta tyylistä kerrotaan hieman lisää tämän sivun lopussa ja tyylioppaassamme.
Sisennysten koko
Scalassa on yleensä tapana käyttää kahden merkin levyistä sisennystä, kuten alkuperäisessä koodissamme ja tulevissa esimerkeissä. Muunkinkokoinen on sisennys mahdollinen, kunhan on johdonmukainen. Tämä seitsemällä välilyönnillä sisennetty koodi toimii kyllä, vaikkei noudatakaan yleistä käytäntöä:
// Toimii, mutta tyyli on tässä poikkeava.
def haukiOnKala(loppukaneetti: String) =
println("Kun hauki on vähärasvainen, sitä voidaan säilyttää pakastettuna jopa 6 kuukautta.")
println("Vertailun vuoksi mainittakoon, että haukea rasvaisemman lahnan vastaava")
println("säilymisaika on vain puolet eli 3 kuukautta.")
println(loppukaneetti)
Jos sinusta tuntuu siltä, että kahden välilyönnin sisennys on luettavuuden kannalta liian vähän, voit kyllä käyttää koodissasi esimerkiksi neljää.
Milloin sisennän?
Koodin jakaminen sisennetyille riveille oli haukiOnKala
-esimerkissä tarpeen, koska
halusimme tulostuskäskyjä samaan funktioon useita peräkkäin. Itse asiassa saman saa
kyllä tehdä, vaikka funktio olisi pienempikin. Esimerkiksi keskiarvo
-funktion voi
kirjoittaa kummalla tahansa seuraavista tavoista.
Tapa 1 (ei rivitystä):
def keskiarvo(eka: Double, toka: Double) = (eka + toka) / 2
Tapa 2 (rivitys):
def keskiarvo(eka: Double, toka: Double) =
(eka + toka) / 2
Monet noudattavat Scala-ohjelmia kirjoittaessaan seuraavia periaatteita:
Jos funktion runko koostuu useasta peräkkäisestä käskystä, rivitetään aina kuten
haukiOnKala
-funktiossa ja tavassa 2 yllä.Samoin jos funktiolla on tilaa muuttavia vaikutuksia (esim. se tulostaa jotain), suositaan tapaa 2, vaikka runko olisi yksirivinenkin.
Tapaa 1 voi käyttää, jos runko koostuu yhdestä rivistä, joka ei vaikuta ohjelman tilaan. Tuo
keskiarvo
-funktio on esimerkki funktiosta, jolle tämä pätee. (Kuitenkin jos rivistä tulisi näin todella pitkä, on silti parempi vaihtaa riviä yhtäsuuruusmerkin jälkeen.)
Tiivistettynä siis: valitaan tapa 1 vain, jos koodista tulee näin yksittäinen, vaikutukseton rivi, joka ei ole ylipitkä.
Kurssimateriaalissakin on tavallisesti noudatettu tätä käytäntöä. Voit itse toimia samoin. Tyylisäännöt voivat aluksi tuntua sekavilta, mutta älä anna sen haitata oman koodin kirjoittamista. Jos se tuntuu helpommalta, voit aivan mainiosti tehdä niin, että käytät tapaa 2 kaikkiin itse laatimiisi funktioihin. Aina saa rivittää ja sisentää!
Mistä tiedän, onko funktio vaikutuksellinen?
Kuvittele tilanne: Ohjelman suoritus on jossakin tietyssä tilassa: muistissa on tallessa joitakin tietoja ja näytöllä näkyy jotakin. Kutsutaan funktiota. Funktion suoritus tulee valmiiksi ja tuottaa paluuarvon, ja hiukan aikaa kuluu. Ollaanko muilta osin samassa tilanteessa kuin ennen kutsun suorittamista?
Jos kutsuttu funktio on esimerkiksi
keskiarvo
-funktio, niin ollaan: saatiin tulos, mutta mikään muu ei ole toisin. Kyseessä on vaikutukseton funktio.Jos se taas on esimerkiksi
println
, niin ei olla: nyt ohjelma on tuottanut rivin tulostetta. Kyseessä on vaikutuksellinen funktio.
Lisäksi voit miettiä tähän tapaan: jos ohjelmalla olisi jo ollut tiedossa kyseisen
funktion paluuarvo, olisiko funktiota edes tarvinnut kutsua? Vaikutuksettoman
keskiarvofunktion tapauksessa ei olisi: olisi sama korvata kutsu keskiarvo(5, 10)
literaalilla 7.5
. Vaikutuksellisen tulostusfunktion println
tapauksessa taas mitään
ei saada tulostettua, ellei funktiota kutsuta.
Funktion rungon lopun voi merkitä erikseenkin
Joskus on kiva selkeyden vuoksi kirjata oikein näkyvästi, mihin funktion koodi
loppuu. Scala tarjoaa mahdollisuuden kirjoittaa rungon perään end
-rivin joka toimii
loppumerkkinä (end marker). Aiemmat esimerkkifunktiomme voi kirjoittaa näinkin:
def keskiarvo(eka: Double, toka: Double) =
(eka + toka) / 2
end keskiarvo
def haukiOnKala(loppukaneetti: String) =
println("Kun hauki on vähärasvainen, sitä voidaan säilyttää pakastettuna jopa 6 kuukautta.")
println("Vertailun vuoksi mainittakoon, että haukea rasvaisemman lahnan vastaava")
println("säilymisaika on vain puolet eli 3 kuukautta.")
println(loppukaneetti)
end haukiOnKala
Loppumerkki alkaa sanalla end
. Kun kyseessä on funktion loppu,
kuten tässä, kirjoitetaan perään funktion nimi. Huomaa, että
loppumerkkejä ei sisennetty syvemmälle, vaan ne ovat linjassa
def
-sanojen kanssa.
Loppumerkkien tarkoitus on auttaa lukijaa, ei muokata ohjelman toimintaa. Yleensä funktion koodin lopun kyllä huomaa ilmankin, joten loppumerkkejä ei useimmiten kirjoiteta. Tässä oppikirjassakaan emme tapaa niitä funktioiden perään kirjoitella; saat kyllä, jos haluat.
Loppumerkeillä on muutakin käyttöä, mistä lisää myöhemmin.
Säestystehtävä
Tehtävänanto
Laadi kaksiparametrinen vaikutuksellinen funktio nimeltä saesta
, joka säestää tulostamaansa
tekstiä äänellä. Molemmat funktion parametrit ovat merkkijonoja: funktio tulostaa ensimmäisen
vastaanottamistaan merkkijonoista ja soittaa toisen.
Funktiota pitää voida käyttää näiden esimerkkien tapaan.
saesta("Nänänänä nänänänä nänänänä nänänänä BÄTMÄÄN!", "[49]<" + "(d<g>)(d<g>)(db<g>)(db<g>)(c<g>)(c<g>)(c#<g>)(c#<g>)" * 2 + "[54]>(FG)-.(FG)---/160")Nänänänä nänänänä nänänänä nänänänä BÄTMÄÄN! saesta("... and introducing acoustic guitar", "[26] >EF#A---G#---F#---E---F#G#---------EF#A---G#---F#---E---F#EF#---------/192")... and introducing acoustic guitar
Rivinvaihdot ovat esimerkin REPL-syötteissä vain estämästä rivien ylipituutta. Ne eivät ole välttämättömät.
Ohjeita ja vinkkejä
Kirjoita ratkaisusi samaan
kierros1.scala
-tiedostoon kuin aiemmissakin tämän luvun tehtävissä.Valitse parametrimuuttujille jotkin järkevät nimet.
Tässä tehtävässä ei ole käytännön merkitystä sillä, kummassa järjestyksessä kirjoitat
println
- japlay
-käskyt funktion runkoon. Voit itse valita, kunhan teet molemmat.Soittamisfunktio on määritelty niin, että äänet soitetaan taustalla, eikä tietokone jää odottamaan soiton loppua ennen kuin suorittaa seuraavan käskyn. Tuloste siis ilmestyy ripeästi ruudulle, vaikka laittaisit soittokäskyn ennen tulostuskäskyä.
Noudata yllä annettua tyyliohjetta ja jaa koodisi usealle riville. Huomaa välimerkkisäännöt.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Työkaluja funktioiden toteuttamiseen
Äskeisistä esimerkeistä jo selvisi, että funktion rungossa voi käyttää aiemmissa
luvussa nähtyjä ohjelmointitekniikoita: aritmeettisia operaattoreita sekä println
-
ja play
-funktioita. Muitakin tuttuja käskyjä voi käyttää. Hieman jäljempänä tässä
luvussa katsotaan esimerkiksi, miten funktion sisään voi määritellä uusia muuttujia.
Lisää funktioiden toteuttamiseen sopivia käskyjä opit kurssin mittaan (esim.
vaihtoehtojen välillä valitseminen, käskyjen toistaminen, yms.).
Hyvin yleistä on käyttää olemassa olevia funktioita uuden funktion toteutuksessa.
Esimerkiksi kahden pisteen (x1,y1) ja (x2,y2) etäisyyden
laskevassa funktiossa voi hyödyntää scala.math
-pakkauksen hypotenuusafunktiota:
def etaisyys(x1: Double, y1: Double, x2: Double, y2: Double) = hypot(x2 - x1, y2 - y1)
Myös aiemmin kohdattuja o1
-pakkauksen kuvafunktioita (luku 1.3) voi hyödyntää omien
funktioiden määrittelyissä.
Kuvan muodostava funktio
Tässä funktio, jolla voi tuottaa erikokoisia punaisia "pallon kuvia":
def punapallo(koko: Int) = circle(koko, Red)
Käyttöesimerkki:
val pallura = punapallo(50)pallura: Pic = circle-shape val isompi = punapallo(300)isompi: Pic = circle-shape show(isompi)
Pikkutehtävä: pystypalkkeja
Laadi kierros1.scala
-tiedostoon funktio pystypalkki
, joka palauttaa kuvan sinisestä
suorakaiteesta, joka on kymmenen kertaa niin monta pikseliä korkea kuin se on leveä.
Funktion tulee ottaa vastaan yksi Int
-tyyppinen parametriarvo, palkin leveys.
Tässä pari käyttöesimerkkiä:
val palkinKuva = pystypalkki(80)palkinKuva: Pic = rectangle-shape show(palkinKuva)show(pystypalkki(180))
Nimeä parametrimuuttuja järkevästi. Testaa funktiotasi REPLissä show
-funktiota apuna
käyttäen. Huomaa, että funktiosi ei pidä kutsua show
-funktiota ja laittaa kuvaa näkyviin,
vaan palauttaa kuva. Tällöin funktiota voi käyttää show
-käskyn kanssa yhdessä kuten
esimerkissä yllä.
Voit kerrata tarvittavia työkaluja luvusta 1.3.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Pikkutehtävä: palkkien kuormittaminen
Jatka edellistä tehtävää ja laadi toinenkin pystypalkki
-niminen funktio. Älä siis
poista alkuperäistä vaan lisää toinen samanniminen funktio, joka ottaa kaksi parametria:
leveyden (Int
) ja värin (Color
). Sitä on tarkoitus käyttää tähän tyyliin:
val mustaPalkki = pystypalkki(80, Black)mustaPalkki: Pic = rectangle-shape show(mustaPalkki)
Tämä jälkimmäinen versio palkkifunktiosta on siis värin suhteen joustavampi (abstraktimpi) mutta vaatii käyttäjältään yhden lisäparametrin. Sekin tuottaa suorakaiteen, joka on kymmenen kertaa niin korkea kuin leveä.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Huomasimme, että Scala sallii meidän määritellä useita samannimisiä funktioita samaan yhteyteen. Moista sanotaan funktion nimen kuormittamiseksi (tai ylikuormittamiseksi; overloading). Tämä on kuitenkin sallittua vain, jos noilla samannimisillä funktioilla on erilaiset parametriluettelot.
Kuormitetut funktiot (tässä: kaksi pystypalkki
-funktiotasi) ovat toisistaan täysin
erilliset. Se kumpi tulee kutsutuksi, riippuu siitä, millaisia parametrilausekkeita
käytät funktiota kutsuessasi.
Pikkutehtäviä: funktioiden tulkintaa
Paikalliset muuttujat
Esimerkki: verofunktio
Laaditaan vaikutukseton funktio, jolla voi määrittää tuloista verotettavan summan yksinkertaisessa verotusjärjestelmässä:
Tiettyyn tulorajaan asti kaikista tuloista maksetaan tietyn perusprosentin verran veroa.
Tulorajan ylittävistä tuloista maksetaan (korkeamman) lisäprosentin verran veroa.
Haluttaisiin, että laadittavaa funktiota verot
voisi kutsua antaen kokonaistulot,
tulorajan sekä perus- ja lisäprosentit parametreiksi kuten tässä:
verot(50000, 30000, 0.2, 0.4)res12: Double = 14000.0 verot(25000, 30000, 0.2, 0.4)res13: Double = 5000.0
Funktion voisi toteuttaa yhdellä rivillä, jolle kirjoitettaisiin koko paluuarvon määrittävä lauseke. Kuitenkin hieman selkeämpi ratkaisu saadaan laittamalla välituloksia muuttujiin:
def verot(tulot: Double, tuloraja: Double, perusprosentti: Double, lisaprosentti: Double) =
val perusosa = min(tuloraja, tulot)
val lisaosa = max(tulot - tuloraja, 0)
perusosa * perusprosentti + lisaosa * lisaprosentti
Tässä määritellään kaksi paikallista muuttujaa (local variable) ja sijoitetaan niille välituloksia arvoiksi.
Kun funktio koostuu useasta käskystä, niin viimeinen käsky määrää, mitä funktio palauttaa. Tässä tapauksessa palautetaan viimeisellä rivillä olevan aritmeettisen lausekkeen arvo. Kaksi aiempaa riviä vain valmistelevat tämän viimeisen lausekkeen evaluointia.
Paikalliset muuttujat ovat käytettävissä vain funktion omasta ohjelmakoodista, eivät
muualta. Funktion kutsujan ei tarvitse niistä tietää, eikä hän voi niitä funktion
ulkopuolelta käyttää, vaikka tietäisikin. Esimerkiksi verot
-funktiota voi kutsua autuaan
tietämättömästi siitä, minkä nimisiä paikallisia muuttujia — jos mitään — sillä on. Ja
seuraava yritys REPLätä funktion sisäisiä muuttujia ei siis onnistu, oli verot
-funktiota
kutsuttu aiemmin tahi ei:
lisaosa * lisaprosentti-- Error: |lisaosa * lisaprosentti |^^^^^^^ |Not found: lisaosa
Itse asiassa paikalliset muuttujat eivät ohjelma-ajon aikana ole edes olemassa kuin kyseistä funktiota suoritettaessa. Ne luodaan funktiokutsua vastaavaan kutsupinon kehykseen, ja niille varattu muisti vapautuu muuhun käyttöön funktiokutsun päättyessä. Tämä konkretisoituu seuraavassa animaatiossa:
Pikkutehtäviä: paikalliset muuttujat
Kurssiarvosanatehtävä
Tehtävänanto
Laadi vaikutukseton funktio, joka määrittää erään kuvitteellisen kurssin kokonaisarvosanan osasuoritusarvosanojen perusteella. Esimerkkikurssillamme kokonaisarvosana on tällöin tehtäväarvosanan (0–4), tenttibonuksen (0 tai 1) ja aktiivisuusbonuksen (0 tai 1) summa; kuitenkaan se ei voi ylittää viitosta.
Funktion on oltava seuraavan spesifikaation mukainen.
Sen nimi on
kurssiarvosana
.Se ottaa parametreikseen tehtäväarvosanan ja tentti- ja aktiivisuusbonukset kokonaislukuina (tässä järjestyksessä).
Se palauttaa kurssin kokonaisarvosanan väliltä 0–5 samaten kokonaislukuna. (Ei tulosta vaan palauttaa.)
Sinun tulee huolehtia siitä, että vaikka osa-arvosanojen summa olisi yli 5, niin kokonaisarvosana ei ole. Kuitenkaan sinun ei tässä tehtävässä tarvitse huolehtia siitä, mitä tapahtuu, jos joku antaakin funktiolle parametriksi virheellisen arvon (esim. negatiivisen tehtäväarvosanan tai liian suuren tenttibonuksen).
Ohjeita ja vinkkejä
Kirjoita taas kierros1.scala
-tiedostoon.
Noudata myös samaa työprosessia, joka on tässä vielä kerran kertauksena:
Varmista ensin, että ymmärrät täsmälleen, mitä funktion on tarkoitus tehdä!
Toteuta funktio vaiheittain: nimi, parametrimuuttujat (nimeä järkevästi!) tyyppeineen, funktion runko.
Alusta uusi REPL-sessio.
Koekäytä funktiotasi eri parametriarvoilla ja palaa kohtaan 1 tarvittaessa.
Palauta vasta, kun olet tyytyväinen koodisi toimintaan.
Eräästä luvun 1.6 esittelemästä ja tässäkin luvussa esiintyneestä matemaattisesta
funktiosta on hyötyä. (Jos kokeilet noita funktioita REPLissä, muista import
scala.math.*
. Tiedostoon kierros1.scala
tuo import
on jo kirjattu, joten
siellä nuo käskyt ovat käytössä.)
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
package
-määrittelyistä
Muokkaamasi tiedoston alussa on rivi, jossa lukee package
-alkuinen merkintä:
package o1.aliohjelmia
Merkintä on varsin itseselitteinen: package
-sanan avulla kirjataan kunkin
Scala-kooditiedoston alkuun, mihin pakkaukseen tuon tiedoston sisältö kuuluu.
package
-määrittelyt ovat tarpeellisia mutta rutiininomaisia. Niitä ei useimpiin
tämän kurssimateriaalin koodinpätkiin ole kirjoitettu mukaan. Oheismoduulien
koodissa ne ovat, ja löydät tällaisen merkinnän valmiina monesta kurssin tiedostosta.
Tiedoksi: kieliversioista
Tässä luvussa olemme kirjoittaneet koodia, joka näyttää tältä:
def verot(tulot: Double, tuloraja: Double, perusprosentti: Double, lisaprosentti: Double) =
val perusosa = min(tuloraja, tulot)
val lisaosa = max(tulot - tuloraja, 0)
perusosa * perusprosentti + lisaosa * lisaprosentti
Vastaavasti kirjoitamme jatkossakin. Kuitenkin jos poukkoilet internetin Scala-sivuilla, törmäät myös koodiin, joka näyttää tältä:
def verot(tulot: Double, tuloraja: Double, perusprosentti: Double, lisaprosentti: Double) = {
val perusosa = min(tuloraja, tulot)
val lisaosa = max(tulot - tuloraja, 0)
perusosa * perusprosentti + lisaosa * lisaprosentti
}
Erona on siis tuossa nuo aaltosulkeet. Muitakin eroja voi tulla vastaan.
Kyse on kieliversioista. Käytämme ajantasaista Scalan kolmosversiota, joka julkaistiin vuonna 2021. Moni muu lähde ei vielä käytä. Vanhoissa Scala-versiossa aaltosulkeita oli käytettävä runsaasti, ja koodi näytti muutenkin hieman toisenlaiselta. Asiasta kertoo vähän lisää tyylioppaamme, johon kannattaa tutustua kurssin alkupuolella muttei välttämättä heti. Esimerkiksi kolmoskierroksen paikkeilla käy hyvin.
Yhteenvetoa
Funktioiden määrittelyyn sisältyvät funktion nimi, parametrimuuttujat tietotyyppeineen sekä funktion runko eli sellaisen algoritmin toteutus, joka hoitaa funktiolle määrätyn tehtävän.
Kun ohjelmaa suoritetaan, tietokone varaa funktiokutsun alkaessa muistista ns. kehyksen.
Funktion parametriarvot tallennetaan muuttujiin, jotka sijaitsevat kehyksessä.
Kehykseen voi myös määritellä muita muuttujia funktion ohjelmakoodissa. Tällaisia muuttujia sanotaan paikallisiksi.
Kehyksistä muodostuu kutsupino (josta lisää seuraavassa luvussa).
Lukuun liittyviä termejä sanastosivulla: funktio, funktiokutsu, funktion runko, loppumerkki; kehys, kutsupino; paikallinen muuttuja, parametrimuuttuja, kuormittaa.
Seuraavassa kaaviossa on mukana eräitä tärkeimpiä funktioiden sisäiseen toimintaan liittyviä käsitteitä.
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.
Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.
Funktion määritelmä alkaa avainsanalla
def
(englannin sanasta define), jonka perään kirjoitetaan ohjelmoijan valitsema funktion nimi. Scalassa funktiot, kuten muuttujatkin, on tapana nimetä pienellä alkukirjaimella.