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 koodissa: def
,
parametrimuuttujat, funktion runko, arvon palauttaminen, paikalliset
muuttujat. Funktiokutsun 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ä haastavin luku. Ensiaskelet koodin kirjoittamisen parissa voivat olla haparoivia, vaikka tässä ei vielä mitään sinänsä hankalaa tehdäkään. Myös välimerkkien käyttö Scala-ohjelmissa vaatii totuttelua.
Pistearvo: A90.
Oheismoduulit: Aliohjelmia.
Muuta: Eräissä tämän luvun 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 ohjelmakoodin moduulin sisältä tiedostosta o1/aliohjelmia/aliohjelmia.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 toimintaperiaate voidaan kuvata suomeksi näin: "Kun keskiarvo
-funktiota
kutsutaan, saadaan palautusarvo laskemalla ensimmäisen parametrin arvo yhteen toisen
parametrin arvon kanssa ja jakamalla summa 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
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. Scalassa tietotyyppien nimet kirjoitetaan
isolla alkukirjaimella.eka
ja toisen toka
.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 katsotaan 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/aliohjelmia.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ä lienee tässä vaiheessa enemmän apua virheiden korjaamisessa.
- Aloita kirjoittamalla
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 Messages-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 näkyvät "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-projekti. 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 aliohjelmia.scala
-tiedostoon:
- Funktio
kuutionTilavuus
, jolle annetaan ainoaksi parametriksi kuution sivun pituus ja joka palauttaa tuonkokoisen kuution tilavuuden. Parametri ja palautusarvo 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 kuitenkin 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ä. Kuitenkin IntelliJ’n editorin käyttö valmistaa paremmin tuleviin tehtäviin ja on tehtävien palauttamisen kannalta kätevämpää. Editorilla luomasi koodi jää myös itsellesi talteen.
Voit omissa kokeiluissasi määritellä funktioita myös REPLissä, mutta muun muassa tämän luvun tehtävät pitää lopulta saada tiedostoon talteen palautettaviksi.
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
Laitetaan 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
aliohjelmia.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 toteutuksessa on useita koodirivejä.
Tällöin koodirivit kirjoitetaan aaltosulkeiden sisään tähän
tapaan. Tässä siis määritellään, että kaikki neljä println
-käskyä
suoritetaan peräjälkeen, kun haukiOnKala
-funktiota kutsutaan.Välilyöntien käyttö on Scalassa teknisesti varsin vapaata. Hyvään tyyliin kuuluu kuitenkin ohjelman osien välisten suhteiden selkeyttäminen välilyönnein; sisentäminen on osa tätä. Scalassa käytetään yleensä kahden välilyönnin kokoista sisennystä.
Entä palautusarvo?
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 palautusarvona.
Palautusarvon määräytyminen monikäskyisissä funktioissa selviää paremmin alempana olevien esimerkkien kautta.
Välimerkit funktioiden määritelmissä
Scala-kieltä opettelevaa voivat ensi alkuun kiusata erikoisilta tuntuvat säännöt, jotka liittyvät välimerkkien käyttämiseen funktioita määriteltäessä. Nämä "pilkkusäännöt" eivät kuulu ohjelmoinnin herkullisimpiin puoliin, mutta joudumme tässä välissä tutustumaan niihin hieman. Aloitetaan yhtäsuuruusmerkistä.
Yhtäsuuruusmerkki
Kun jatkossa laadit funktioita, muista aina kirjoittaa yhtäsuuruusmerkki parametriluettelon ja funktion rungon väliin. Sen unohtamisesta voi nimittäin seurata hankalastikin löydettäviä bugeja.
Aaltosulkeet ja rivinvaihdot
Aaltosulkeiden käyttö oli haukiOnKala
-esimerkissä välttämätöntä, koska tulostuskäskyjä
haluttiin laittaa samaan funktioon useita. Itse asiassa rivinvaihtoja ja/tai aaltosulkeita
saa kyllä käyttää, vaikka funktio olisi pienempikin. Esimerkiksi keskiarvo
-funktion voi
kirjoittaa millä tahansa seuraavista tavoista:
Tapa 1:
def keskiarvo(eka: Double, toka: Double) = (eka + toka) / 2
Tapa 2:
def keskiarvo(eka: Double, toka: Double) =
(eka + toka) / 2
Tapa 3:
def keskiarvo(eka: Double, toka: Double) = {
(eka + toka) / 2
}
Monet noudattavat Scala-ohjelmia kirjoittaessaan seuraavia periaatteita:
- Kun funktion runko koostuu yhdestä lausekkeesta eikä funktio
vaikuta ohjelman tilaan, niin aaltosulkeita ei käytetä.
- Jos lauseke on lyhyt, niin sen voi sijoittaa
suoraan
def
-riville aaltosulkeita käyttämättä. Tapa 1 on siis suositeltu tapa kirjoittaakeskiarvo
-funktio. - Jos rivistä muuten tulisi kovin pitkä, laitetaan yhtäsuuruusmerkin perään rivinvaihto (kuten tavassa 2).
- Jos lauseke on lyhyt, niin sen voi sijoittaa
suoraan
- Kun funktion runko koostuu useasta eri käskystä tai sillä on
tilaa muuttavia vaikutuksia (esim. se tulostaa jotain), niin
käytetään rivinvaihtoja, aaltosulkeita ja sisennyksiä siten kuin
tehtiin
haukiOnKala
-esimerkissä ja tavassa 3 yllä.
Kurssimateriaalissakin on tavallisesti noudatettu näitä käytäntöjä. 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 3 kaikkiin itse laatimiisi funktioihin. Aaltosulkeet saa aina kirjoittaa!
Välimerkeistä kootusti
Tässä Scala-välimerkkiohjeilla varustettu versio edellisen luvun taulukosta. Siirtämällä hiiren kursorin alleviivatun kohdan päälle saat lisätietoja.
Vaikuttaako funktio tilaan? | Palauttaako arvon? | Kurssin termi | Yhtäsuuruus- merkki rungon eteen? | Aaltosulkeet rungon ympärille? | Rivinvaihdot ja sisennykset? |
---|---|---|---|---|---|
Ei vaikuta koskaan | Palauttaa | Vaikutukseton funktio | Kyllä | Ainakin kun useita käskyjä. | Aina kun aaltosulkeet. Myös pitkiin yksirivisiin. |
Ei palauta | (Jos funktio ei vaikuta tilaan eikä palauta arvoa, niin se ei voi olla järin hyödyllinen kuin erikoistilanteissa. Tällaisia funktioita sinun ei tarvitse tällä kurssilla laatia.) | ||||
Vaikuttaa ainakin joskus | Palauttaa | Vaikutuksellinen funktio | Kyllä | Kyllä | Kyllä |
Ei palauta | Vaikutuksellinen funktio | Kyllä | Kyllä | Kyllä |
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 palautusarvon, 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 yhden rivin tulostetta. Kyseessä on vaikutuksellinen funktio.
Syventäviä hakusanoja kiinnostuneille
Aiheesta löytyy netistä lisää mm. termeillä referential transparency ja side effect. Ks. myös luku 10.2.
Lisäksi voit miettiä tähän tapaan: jos ohjelmalla olisi jo ollut tiedossa kyseisen
funktion palautusarvo, 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.
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
aliohjelmia.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ä laitat
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 aliohjelmia.scala
-tiedostoon funktio pystypalkki
, joka palauttaa kuvan
sinisestä suorakaiteesta, joka on kymmenen kertaa niin monta pikseliä korkea kuin
se on leveä.
Funktion tulee ottaa yksi Int
-tyyppinen parametri, 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). Kuitenkin tämä on 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 palautusarvon 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
}
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<console>:14: error: not found: value lisaosa lisaosa * lisaprosentti ^
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 aliohjelmia.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 aliohjelmia.scala
tuo import
on jo kirjattu, joten
siellä nuo käskyt ovat käytössä.)
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
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 kutsua varten 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; 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, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä ja Aleksi Vartiainen.
Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.
Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista suunnittelivat Juha Sorva ja Teemu Sirkiä. Teemu Sirkiä ja Riku Autio toteuttivat ne apunaan Teemun aiemmin rakentamat työkalut Jsvee- ja Kelmu.
Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset laati Juha Sorva.
O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.
Tapa, jolla käytämme O1Libraryn työkaluja (kuten Pic
) yksinkertaiseen graafiseen
ohjelmointiin, on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi
oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.
Oppimisalusta A+ luotiin alun perin Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Nykyään tätä avoimen lähdekoodin projektia kehittää Tietotekniikan laitoksen opetusteknologiatiimi ja tarjoaa palveluna laitoksen IT-tuki. Pääkehittäjänä on tällä hetkellä Markku Riekkinen, jonka lisäksi A+:aa ovat kehittäneet kymmenet Aallon opiskelijat ja muut.
A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen ovat luoneet Nikolai Denissov, Olli Kiljunen, Nikolas Drosdek, Styliani Tsovou, Jaakko Närhi ja Paweł Stróżański yhteistyössä Juha Sorvan, Otto Seppälän, Arto Hellaksen ja muiden kanssa.
Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.
Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.
def
(englannin sanasta define), jonka perään kirjoitetaan ohjelmoijan valitsema funktion nimi. Scalassa funktiot, kuten muuttujatkin, on tapana nimetä pienellä alkukirjaimella.