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

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.

Oheisprojektit: Aliohjelmia.

../_images/sound_icon.png

Muuta: Eräissä tämän luvun kohdissa on kaiuttimista tai kuulokkeista hyötyä. Aivan pakolliset ne eivät ole.

../_images/person01.png

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 aiemmista luvuista tuttuun tapaan, valitse käyttöön Aliohjelmia-projekti ja anna käsky import o1._.

Löydät lukuun liittyvän ohjelmakoodin myös projektin sisältä tiedostosta o1/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. Eclipsehän tekee Scala-koodille samoin joskin eri väreillä.)

def keskiarvo(eka: Double, toka: Double) = (eka + toka) / 2
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.
Tässä ilmoitetaan, 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. Scalassa tietotyyppien nimet kirjoitetaan isolla alkukirjaimella.
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 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: "Rivillä 15 olevassa 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

Laaditaan pieni vaikutukseton funktio, jota voi käyttää vaikkapa näin:

huuda("Jipii")res1: String = Jipii!
val tulos = huuda("Hahaa")tulos: String = Hahaa!
val kovempaa = huuda(tulos)kovempaa: String = Hahaa!!

Alla on pohja funktion toteutukselle. Siitä on "piilotettu" viisi kohtaa laittamalla kunkin niistä tilalle kolme kysymysmerkkiä.

??? ???(lause: ???) = ??? + ???

Vaihda kysymysmerkkien tilalle muut koodinpätkät siten, että syntyy esimerkin mukaisesti toimivan funktion määrittely. Kukin kysymysmerkkikohta korvautuu eri koodinpätkällä. Käytä mallina ylempänä annettua keskiarvo-funktion määrittelyä.

Kirjoita alle toimiva toteutus funktiolle.

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 voidaan 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 esiin Eclipse, sieltä projekti Aliohjelmia, ja sieltä tiedosto o1/aliohjelmia.scala. Huomaat tiedoston alkupäässä merkityn kohdan, johon tässä luvussa kirjoitetaan funktioita.

Koodin kirjoittaminen

  1. 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.

  2. Kirjoita funktion koodi merkittyyn paikkaan. Tämä tehtävä ratkeaa yhdellä koodirivillä.

    1. Aloita kirjoittamalla def ja funktion nimi.
    2. Kirjoita parametrimäärittelyt. Nimeä parametrimuuttujat itse mielekkäällä tavalla.
    3. Kirjoita funktion runko. Tässä se on yksi aritmeettinen lauseke, joka tekee halutun yksikkömuunnoksen. (Älä tulosta arvoa vaan palauta se!)

    Eclipsen virheilmoituksista

    Koodia editoidessasi huomannet, että silloin tällöin näkyviin ilmestyy punaisia merkintöjä, mm. sahalaitaisia alleviivauksia: error_underline. Tämä on Eclipsen tapa ilmoittaa havaitsemistaan ongelmista. Älä häkelly, vaikka näitä merkintöjä ilmestyisi heti, kun alat koodia kirjoittaa. Eclipse nimittäin varoittelee koodin nykytilan perusteella, ja keskeneräinen koodi tuottaa helposti virheilmoituksia. Esimerkiksi siinä vaiheessa, kun olet kirjoittanut vasta def-sanan, Eclipse 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.

    Eclipsen editorin alapuolelta löytyy Problems-välilehti, jonne tulee näkyviin kustakin virhemerkinnästä tarkempia tietoja. Samoja virheilmoituksia voi katsoa myös jättämällä hiiren kursorin editorin marginaalissa näkyvien error_symbol-symbolien päälle. Virheilmoitustekstien tulkinta on kurssilla tähän mennessä opitun perusteella tosin valitettavasti usein vaikeaa; kurssimateriaalin esimerkeistä lienee tässä vaiheessa enemmän apua virheiden korjaamisessa.

  3. Huolehdi, ettei koodiin jää Eclipsen 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?

  4. Muista tallentaa tiedosto!

Huom.

Ennen kuin yrität ajaa ohjelmaasi, huolehdi aina ensin siitä, että olet poistanut Eclipsen värittämät virheet ohjelmasi tiedosto(i)sta. Muuten seuraava vaihe eli testaaminen ei onnistu lainkaan!

Testaus

  1. Käynnistä REPL tuttuun tapaan (esim. Alt+F11) ja valitse käyttöön Aliohjelmia-projekti.

  2. Ota funktiosi käyttöön joko käskyllä import o1._ tai import o1.metreiksi.

  3. 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, on sinun ladattava muutettu versio REPLiin uudestaan, jotta sitä voit testata siellä. Voit esimerkiksi joko:

    • sulkea REPLin ja käynnistää sen uudestaan, tai
    • painaa REPLin oikeassa yläkulmassa olevaa stop-nappia. Tämä nappi ei vaikuta tekevän juuri mitään, mutta huomaamattomasti se kuitenkin "nollaa" REPLin, joten mm. vanhat import-käskyt eivät enää ole voimassa.

    Kun olet tehnyt jommankumman edellisistä, anna import-käsky uudelleen. Muista, että voit kelata aiempia saman session aikana annettuja käskyjä ylä- ja alanuolella pitäessäsi samaan aikaan Ctrl-näppäintä pohjassa. (Mac-ympäristöissä Ctrl+Cmd ja nuolet.)

    Voit myös kokeilla muita REPLin oikean yläkulman nappuloita. Esimerkiksi Relaunch Interpreter and Replay History -nappula käynnistää REPLin uudestaan ja ajaa uudestaan kaikki ne komennot, jotka olit "buutatun" session aikana antanut. Tämä voi olla kätevää, kun muutat koodiasi ja haluat kokeilla samoja komentoja uudella koodilla.

Palauttaminen

Kun funktiosi toimii, ole iloinen ja käytä alla olevaa lomaketta tehtävän palauttamiseen.

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 ovat Double-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 Eclipsen 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 sikäli näppärä, ettei tarvitse "nollata" REPLiä lainkaan eikä antaa import-käskyjä. Kuitenkin Eclipsen editorin käyttö valmistaa paremmin tuleviin tehtäviin ja on tehtävien palauttamisen kannalta kätevämpää. Luomasi koodi myös jää näin 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.

Mikä seuraavista vastaa modulo-operaattorin toimintaa parhaiten?

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 alla olevaan tehtävään.

Shakkilautatehtävä: johdanto

../_images/shakki1.png

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
../_images/shakki2.png

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. Muista import.

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.
Tämä on myös ensimmäinen esimerkki, jossa näemme usealle riville jakautuvan sisäkkäisen rakenteen ohjelmakoodissa. Tällaiset rakenteet on Scalassa ja ohjelmoinnissa yleisestikin tapana sisentää (indent) tähän tapaan.

Välilyöntien käyttö on Scalassa teknisesti varsin vapaata, mutta hyvään tyyliin kuuluu niiden käyttö ohjelman osien välisten suhteiden selkeyttämiseen; 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 kirjoittaa keskiarvo-funktio.
    • Jos rivistä muuten tulisi kovin pitkä, laitetaan yhtäsuuruusmerkin perään rivinvaihto (kuten tavassa 2).
  • 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. Aaltosulut 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? Aaltosulut rungon ympärille? Rivinvaihdot ja sisennykset?
Ei vaikuta koskaan Palauttaa Vaikutukseton funktio Kyllä! Ainakin kun useita käskyjä. Aina kun aaltosulut. 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.

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- ja play-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.

Palauttaminen

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:

import o1._import o1._
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

Vastaa kunkin funktion kohdalla kaikki ne vaihtoehdot, jotka kuvaavat, millainen funktio on kyseessä. Tässä ensimmäinen funktio:

def etaisyys(x1: Double, y1: Double, x2: Double, y2: Double) = hypot(x2 - x1, y2 - y1)
def punapallo(koko: Int) = circle(koko, Red)
def kokeilu1(luku: Int) = {
  println("Luku on: " + luku)
}

Huomaa seuraavan kokeilufunktion parametrimuuttujan tyyppi: parametriarvoksi tulee antaa viittaus kokonaislukuja sisältävään puskuriin.

def kokeilu2(lukuja: Buffer[Int]) = {
  lukuja(0) = 100
}
def kokeilu3(luku: Int) = {
  println("Luku on: " + luku)
  luku + 1
}
Mikä seuraavista kuvaa parhaiten sitä, miten funktiokutsun suorittaminen alkaa (siinä tavallisessa ohjelmien suoritusmallissa, joka on tässä luvussa esitelty)? Kertaa tarpeen mukaan luvun alussa olevasta animaatiosta. Voit myös pohtia, millaisissa tilanteissa on merkitystä sillä, evaluoidaanko kaikki parametrilausekkeet ennen funktiokutsun alkua vai vasta funktiokutsun aikana.

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
}
Tässä määritellään kaksi paikallista muuttujaa (local variable) ja sijoitetaan niille välituloksia arvoiksi.
Tässä esimerkissä kannattaa kiinnittää huomio myös siihen, että 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<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

Tarkastele, miten välituloksia varten luodut paikalliset muuttujat ja parametrimuuttujat kuvattiin animaatiossa. Arvioi sitten, pitääkö seuraava väite paikkansa vai ei: "Parametrimuuttujat ovat paikallisia muuttujia nekin."

Vastaa kunkin funktion kohdalla kaikki ne vaihtoehdot, jotka kuvaavat, millainen funktio on kyseessä.

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
}
def kokeilu4(sana: String) = {
  var luku = 1
  println(sana + ": " + luku)
  luku = luku + 1
  println(sana + ": " + luku)
  luku = luku + 1
  println(sana + ": " + luku)
  luku
}
def kokeilu5(aluksi: Int) = {
  var luku = aluksi
  luku = luku + 1
  luku = luku + 1
  luku = luku + 1
  luku
}

Otetaan tehtäväksi laatia hieman luvussa 1.6 käyttämääsi kaanon-funktiota muistuttava mutta toteutukseltaan yksinkertaisempi funktio, jolla voi soittaa tietyn melodian kahdella eri soittimella peräjälkeen (ei päällekkäin). Funktio toimisi näin:

val duetto = kahdella("g#e--f#c#----", 26, 66, 5)duetto: String = [26]g#e--f#c#----     [66]g#e--f#c#----
play(duetto)
Funktio palauttaa merkkijonon, johon sisältyy ensimmäisenä parametrina annettu merkkijono kahdesti.
Toinen ja kolmas parametri määräävät soittimet. kahdella-funktiomme tekee niiden perusteella palauttamaansa merkkijonoon hakasulkumerkinnät, jotka play-funktio osaa tulkita ohjeeksi soittaa pätkä ensin yhdellä sitten toisella instrumentilla.
Viimeinen parametri määrää, kuinka pitkä tauko väliin laitetaan.

Tässä on kahdella-funktiolle melkein toimiva toteutus, joka löytyy myös tiedostosta aliohjelmia.scala.

def kahdella(melodia: String, eka: Int, toka: Int, tauonPituus: Int) = {
  val melodiaEkalla = "[" + eka + "]" + melodia
  val melodiaTokalla = "[" + toka + "]" + melodia
  val tauko = " " * tauonPituus
  val kahdestiSoitettuna = melodiaEkalla + tauko + melodiaTokalla
}

Tässä toteutuksessa on ohjelmointivirhe, joka on aloittelijalle tyypillinen. Tutki koodia huolellisesti; voit myös kokeilla käyttää sitä REPLissä. Valitse seuraavista väittämistä kaikki paikkansa pitävät. Voit korjata funktiototeutuksen tiedostoonkin.

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:
    1. Varmista ensin, että ymmärrät täsmälleen, mitä funktion on tarkoitus tehdä!
    2. Toteuta funktio vaiheittain: nimi, parametrimuuttujat (nimeä järkevästi!) tyyppeineen, funktion runko.
    3. Alusta REPL-sessio ja käytä import-käskyä ottaaksesi funktiosi käyttöön.
    4. Koekäytä funktiotasi eri parametriarvoilla ja palaa kohtaan 1 tarvittaessa.
    5. 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ä.

Palauttaminen

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!

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: (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 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.

Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.

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