Luku 1.8: Funktioista, tyypeistä ja virheistä

../_images/person01.png

Lisää funktiokutsuista ja kutsupinosta

Tarkastellaan seuraavaa ohjelmakoodia, jossa itse laadittu funktio isoinEtaisyys ottaa parametreiksi kolmen pisteen x- ja y-koordinaatit ja selvittää, mikä on pisin etäisyys näiden pisteiden välillä. Tämä funktio hyödyntää toista itse laadittua funktiota etaisyys, joka jo edellisessä luvussakin nähtiin ja jolla voi määrittää kahden pisteen etäisyyden.

def etaisyys(x1: Double, y1: Double, x2: Double, y2: Double) = hypot(x2 - x1, y2 - y1)

def isoinEtaisyys(x1: Double, y1: Double, x2: Double, y2: Double, x3: Double, y3: Double) =
  val eka = etaisyys(x1, y1, x2, y2)
  val toka = etaisyys(x1, y1, x3, y3)
  val kolmas = etaisyys(x2, y2, x3, y3)
  max(max(eka, toka), kolmas)

Funktiot voivat olla "sisäkkäin" kahdella eri tapaa:

Funktiokutsulausekkeet voivat olla ohjelmakoodissa sisäkkäin.

Lisäksi funktiota voi kutsua toisen funktion toteutuksesta, kuten vaikkapa tämän esimerkin isoinEtaisyys kutsuu etaisyys-funktiota (kolmesti).

Sisäkkäisyydestä ja tuosta esimerkkifunktiosta kertoo lisää tämä animaatio.

Tykkään miten animaatioista näkee, miten tietokone "ajattelee", eli itseasiassa ei ajattele vaan ra’asti noudattaa sisältä-ulos järjestystä sisäkkäisille funktioille. Koodin lukeminen helpottuu, kun osaa ajatella kuin tietokone.

Kokoava funktioharjoite ennen koodausrupeamaa

Kun etenemme kohti monimutkaisempia ja hyödyllisempiä ohjelmia, on välttämätöntä, että kykenet tekemään luotettavasti päätelmiä siitä, miten annettu koodi käyttäytyy ajettaessa. Tai itse kirjoittamasi mutta virheellinen koodi!

Tutustu seuraavaan ohjelmakoodiin. Se ei tee mitään hyödyllistä, mutta toimii harjoitteena sisäkkäisistä funktiokutsuista, paluuarvoista ja tulostamisesta.

Mitkä rivit tulostuvat, kun suoritetaan käsky testi2("kissa")? Kirjoita tähän tulosteet oikeassa järjestyksessä, ei tulostuskäskyt sisältäviä koodirivejä.

Montako kertaa yhteensä mainitun testi2-funktiokutsun suorituksen aikana tulee luoduksi joko huuda-, testi1- tai testi2-kutsuun kytkeytyvä kehys? (Tämä tarkoittaa, että alimmaista animaatiossa näkyvää kehystä, josta funktioita kutsutaan, ei tässä lueta mukaan.)

Montako huuda-, testi1- tai testi2-funktiokutsua yhteensä tehdään mainitun testi2-funktiokutsun suorituksen aikana?

Montako huuda-, testi1- tai testi2-kutsuun liittyvää kehystä on enimmillään yhtaikaisesti kutsupinossa mainitun kutsun suorituksen aikana? (Alimmaista kehystä, josta näitä funktioita lähdetään kutsumaan, ei tässä lueta mukaan.)

Montako huuda-, testi1- ja testi2-funktiokutsua on enimmillään yhtaikaisesti kesken mainitun funktiokutsun suorituksen aikana?

Voiko kutsupinon eri kehyksissä olla keskenään samannimisiä muuttujia?

Seuraavat kysymykset käyvät ehkä esimerkiksi siitä, miksi kutsupinon ja kehysten tunteminen auttaa tulkitsemaan ohjelmakoodia.

Tutkitaan tällaista esimerkkifunktiota:

def kokeilu(luku: Int) =
  var a = luku
  println(a)
  a = a + 1
  println(a)
  a - 1

Tässä tuon funktion käyttökokeilu:

val a = 10
println(kokeilu(100))
println("a:n arvo on nyt: " + a)

Kun äskeiset kolme riviä määrätään suoritettavaksi (esim. REPLissä), niin mitä kolmannen rivin käsky tulostaa ja miksi?

Jos tehtävä tuntuu vaikealta, koeta pohtia, millaisia kehyksiä ajettaessa syntyy ja mitä muuttujia niissä kussakin on.

def laske(x: Int) = 2 * x + apu(10) + 100

def apu(y: Int) = x + y

Arvioi yllä olevia funktioita sen perusteella, mitä olet oppinut funktioista, muuttujista ja kutsupinosta. Esitä arviosi tai ainakin perusteltu arvaus siitä, mitkä seuraavista väittämistä pitävät paikkansa. Muista taas lukea saamasi palaute. Halutessasi voit myös kokeilla funktioita esimerkiksi kopioimalla nuo määrittelyt REPLiin.

Kaverisi haluaa laatia Scala-funktion nimeltä vaihdaSisallot, joka toimii tämän käyttöesimerkin tapaan:

var a = 1a: Int = 1
var b = 2b: Int = 2
vaihdaSisallot(a, b)ares0: Int = 2
bres1: Int = 1
var c = 3c: Int = 3
vaihdaSisallot(a, c)ares2: Int = 3
cres3: Int = 2

Kaverin tarkoitus siis on, että funktiolle voi ilmoittaa mitkä tahansa kaksi Int-arvoista var-muuttujaa funktion itsensä ulkopuolelta, ja funktio sitten vaihtaa näiden muuttujien sisältämät arvot keskenään.

Tässä kaksi hahmotelmaa tuollaisesta funktiosta.

def vaihdaSisallot(luku1: Int, luku2: Int) =    // Hahmotelma 1
  val apu = luku1
  luku1 = luku2
  luku2 = apu
def vaihdaSisallot(a: Int, b: Int) =            // Hahmotelma 2
  val apu = a
  a = b
  b = apu

Kumpikaan hahmotelmista ei toimi. Miksei? Valitse kaikki syyt, jotka pitävät paikkansa. Arvioi tilannetta näkemiesi koodiesimerkkien ja animaatioiden valossa, kokeile REPLissä ja keskustele tarvittaessa parisi tai kurssihenkilökunnan kanssa. Muista lukea vastauksesta saamasi palaute.

Ohjelmointitehtävä: liigapisteet ja joukkueenTiedot

Tässä tehtävässä laadit kaksi funktiota hyödyntäen ensimmäistä rakennuspalikkana toista toteuttaessasi.

Programming is like building with smart Lego that you design yourself!

—alkuperä tuntematon

Tehtävänanto

Laadi tiedostoon kierros1.scala kaksi vaikutuksetonta funktiota, joilla voi laskea urheilujoukkueen liigapisteet sen pelitulosten perusteella.

Ensimmäisen funktion on oltava seuraavanlainen:

  • Sen nimi on liigapisteet.

  • Se ottaa parametreikseen voittojen ja tasapelien lukumäärät kokonaislukuina (tässä järjestyksessä).

  • Se palauttaa (ei tulosta println-käskyllä!) joukkueen liigapisteet kokonaislukuna. Voitosta saa kolme pistettä, tasapelistä yhden ja tappiosta ei yhtään.

Toisen funktion taas on toimittava näin:

  • Sen nimi on joukkueenTiedot.

  • Se ottaa parametreikseen, tässä järjestyksessä, joukkueen nimen (merkkijono) sekä voittojen, tasapelien ja tappioiden lukumäärät (kokonaislukuina).

  • Se palauttaa (ei tulosta!) merkkijonon, joka on muotoa "Nimi: X/N voittoa, Y/N tasapeliä, Z/N tappiota, P pistettä". Esimerkiksi silloin, kun annetut parametriarvot ovat "Liverpool FC", 8, 7 ja 7, niin palautetaan merkkijono: "Liverpool FC: 8/22 voittoa, 7/22 tasapeliä, 7/22 tappiota, 31 pistettä"

Työvaiheet

  1. Laadi liigapisteet-funktio. Noudata samoja työvaiheita kuin edellisen luvun 1.7 ohjelmointitehtävissä. Älä unohda testata, että funktiosi toimii ennen kuin jatkat.

  2. Laadi ja testaa joukkueenTiedot-funktio samaan tapaan. Lisäohjeita ja vinkkejä:

    • Kun laadit useammasta käskystä koostuvan funktiorungon, muista sisennykset.

    • Käytä aiemmin laatimaasi liigapisteet-funktiota joukkueenTiedot-funktion sisältä laskeaksesi joukkueen pisteet.

    • Koodista tulee luettavampi, jos käytät paikallista muuttujaa apuna pelien kokonaislukumäärän tallentamiseen.

  3. Palauta tehtävä, kun olet laatinut ja testannut molemmat funktiot.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Lisää tehtäviä: funktio toisen rakennuspalikkana

Lisätreeni: Jalat ja tuumat toiseen suuntaan

Tämä harjoitus jatkaa luvun 1.7 jalat ja tuumat -teemaa kolmella yksirivisellä pikkufunktiolla, joista ensimmäistä hyödynnetään kahdessa jälkimmäisessä.

Laadi vaikutukseton funktio tuumiksi, joka palauttaa annettua metrimäärää vastaavan määrän tuumia (kun tuuma on 2,54 cm). Esimerkkejä:

tuumiksi(1.8)res4: Double = 70.86614173228347
tuumiksi(0.0254)res5: Double = 1.0

Laadi sitten kaksi funktiota, joiden avulla voi selvittää, paljonko annettu metrimäärä on kokonaisten jalkojen ja ylijäävien tuumien yhdistelmänä. Esimerkiksi 1,8 m on viisi jalkaa ja noin yksitoista tuumaa. Funktioiden tulee toimia näin:

kokonaisetJalat(1.8)res6: Double = 5.0
tuumiaYli(1.8)res7: Double = 10.866141732283475

Hyödynnä toteutuksessasi tuumiksi-funktiota. Kertaa myös scala.math-pakkauksen apufunktioita luvusta 1.6 tarpeen mukaan.

Kirjoita nämäkin funktiot tiedostoon kierros1.scala.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Haastavampi versio edellisestä

Selvitä omatoimisesti, miten Scalassa voi käyttää pareja (pair) ja sovella oppimaasi laatiaksesi hieman erilaisen ratkaisun äskeiseen lisätreeniin.

Toteuta tuumiksi-funktio kuten yllä ehdotetaan. Toteuta sen lisäksi funktio jaloiksiJaTuumiksi, jossa yhdistyy kokonaisetJalat- ja tuumiaYli-funktioiden toiminnallisuus. Funktion tulee palauttaa pari, jonka jäseninä ovat sekä kokonaisten jalkojen määrä että yli jääneet tuumat. Näin:

jaloiksiJaTuumiksi(1.8)res8: (Double, Double) = (5.0,10.866141732283467)
jaloiksiJaTuumiksi(0.0254)res9: (Double, Double) = (0.0,1.0)

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Palaamme parien pariin luvussa 9.2.

Ohjelmointitehtävä: sanallinenArvosana

Palataan arvosanoihin ja luodaan vaikutukseton funktio, jolla voi tuottaa sanallisen kurssiarvosanan kuvitteelliselle esimerkkikurssillemme. Tässä on funktiolle pohja:

def sanallinenArvosana(tehtavaarvosana: Int, tenttibonus: Int, aktiivisuusbonus: Int)
  val kuvaukset = Buffer("hylätty", "välttävä", "tyydyttävä", "hyvä", "erittäin hyvä", "erinomainen")
  // TÄYDENNÄ RATKAISUSI TÄHÄN. VOIT POISTAA TÄMÄN KOMMENTIN.

Funktion tulisi toimia näin:

  • Se ottaa parametreikseen tehtäväarvosanan sekä tentti- ja aktiivisuusbonukset aivan kuten luvun 1.7 kurssiarvosana-funktiokin otti.

  • Kokonaisluvun sijaan funktion tulee palauttaa merkkijono, joka kuvaa kyseistä arvosanaa: esim. kakkonen on "tyydyttävä" ja viitonen "erinomainen".

Toteuta funktio toimivaksi:

  • Kopioi yllä annettu pohjakoodi kierros1.scalaan.

  • Annetussa pohjakoodissa on pieni mutta vakava virhe! Korjaa se.

    • IntelliJ’n kuvaus tästä virheestä ei ole loistava, syystä johon palaamme tämän luvun lopussa. Mutta korjaus on yksinkertainen.

  • Täydennä funktion runko. Käytä apuna koodissa jo määriteltyä puskuria sekä luvussa 1.7 laatimaasi kurssiarvosana-funktiota. Nämä yhdistämällä ratkaisu on suoraviivainen.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Ohjelmointitehtävä: tuplaaPisteet

Seuraava funktio on vaikutuksellinen: se muuttaa parametriksi annetun puskurin sisältöä. (Tässä mielessä se on siis samankaltainen kuin luvun 1.6 poistaNegatiiviset.)

Tehtävänanto

Tarkastellaan kuvitteellista peliohjelmaa, jossa usea keskenään kilpaileva pelaaja kerää pisteitä. Pelin kuluessa tietyn pelaajan pisteet voivat välillä tuplautua ja välillä niitä voidaan vähentää. Tässä tehtävässä sinun tulee laatia tiedostoon kierros1.scala vaikutuksellinen pisteiden tuplausfunktio, joka toimii seuraavasti:

  • Sen nimi on tuplaaPisteet.

  • Se ottaa ensimmäiseksi parametriksi viittauksen puskuriin (Buffer), jonka alkioina on kunkin pelaajan nykyinen pistemäärä kokonaislukuna.

  • Se ottaa toiseksi parametriksi kokonaisluvun, joka määrittää kenen pelaajista pisteet tuplataan: 1 tarkoittaa ekan pelaajan pisteitä, 2 tokan ja niin edelleen.

  • Se muuttaa annetun puskurin sisältöä kaksinkertaistamalla kyseisen pelaajan pisteet.

Käyttöesimerkki:

val osallistujienPisteet = Buffer(2, 10, 5, 2)osallistujienPisteet: Buffer[Int] = ArrayBuffer(2, 10, 5, 2)
tuplaaPisteet(osallistujienPisteet, 3)tuplaaPisteet(osallistujienPisteet, 4)osallistujienPisteetres10: Buffer[Int] = ArrayBuffer(2, 10, 10, 4)

Ohjeita ja vinkkejä

  • Funktion ensimmäisen parametrin tyypiksi pitää merkitä, että se on puskuri, jonka alkiot ovat kokonaislukuja. Käytä hakasulkeita puskurin tyyppiparametrin ympärillä kuten luvussa 1.5.

  • Toinen parametri osoittaa kohdepelaajan ykkösestä alkavalla numeroinnilla kun taas puskurien indeksit alkavat nollasta (luku 1.5). Sinun täytyy huomioida tämä saadaksesi aikaan spesifikaation mukaisesti toimivan funktion.

  • Älä välitä erikoistapauksista kuten siitä, mitä tapahtuu, jos metodille antaa parametriksi liian suuren tai pienen pelaajanumeron. Yleensäkään kurssin tehtävissä ei tarvitse välittää mistään sellaisista virhetilanteista, joita ei ole erikseen pyydetty huomioimaan.

  • Voit olettaa, että jokaisella pelaajalla on aina vähintään yksi piste.

  • Funktion ei tarvitse palauttaa mitään.

  • Funktion rungoksi riittää yksi puskuriin uuden arvon sijoittava käsky.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Ohjelmointitehtävä: sakko

Jatketaan saman kuvitteellisen pelin parissa. Nyt laadittavaksi tulee vaikutuksellinen funktio, jolla vähennetään tietyn pelaajan pisteitä. Sen tulee toimia näin:

  • Funktion nimi on sakko.

  • Kuten edellisenkin funktion tapauksessa, ensimmäisellä parametriarvolla ilmoitetaan pisteitä sisältävä puskuri, jota muutetaan, ja toisella parametriarvolla pelaajan numero.

  • Kolmas parametri ilmoittaa, montako pistettä yritetään vähentää. Voit olettaa tämän luvun positiiviseksi.

  • Kuitenkin pelin säännöt määräävät, ettei pelaajan pisteitä voi koskaan vähentää nollaan tai sen alle, vaan pisteitä on jäätävä ainakin yksi. Jos vähennysyritys on suurempi, pelaajan pisteet vähenevät ykköseen.

  • Funktio palauttaa kokonaisluvun, joka kertoo, montako pistettä onnistuneesti vähennettiin.

Alla on käyttöesimerkki:

val osallistujienPisteet = Buffer(2, 10, 5, 2)osallistujienPisteet: Buffer[Int] = ArrayBuffer(2, 10, 5, 2)
sakko(osallistujienPisteet, 2, 3)res11: Int = 3

Vähennetään pelaajan numero 2 pisteitä kolmella. Paluuarvo kertoo, että kolme pistettä on saatu vähennettyä.

Vähennyksen näkee myös katsomalla pistepuskurin sisältöä, jossa kakkospelaajan pisteet ovat pudonneet seitsemään:

osallistujienPisteetres12: Buffer[Int] = ArrayBuffer(2, 7, 5, 2)

Annetaan samalle pelaajalle vielä kahdentoista pisteen sakko:

sakko(osallistujienPisteet, 2, 12)res13: Int = 6
osallistujienPisteetres14: Buffer[Int] = ArrayBuffer(2, 1, 5, 2)

Kuitenkin vain kuusi pistettä saadaan vähennettyä...

... koska pelaajalle pitää aina jäädä vähintään yksi piste.

Jos tehtävä tuntuu sinusta selvältä, voit toki kirjoittaa ratkaisun koko tehtävään saman tien. Muussa tapauksessa suosittelemme kaksivaiheista ratkaisutapaa, jossa asteittain lähestyt oikeaa ratkaisua:

Vaihe 1/2: älä mieti paluuarvoa

Laadi funktiosta versio, joka ainoastaan vähentää kohdepelaajan pisteitä puskurissa. Älä vielä välitä siitä, palauttaako funktio oikean arvon.

Voit käyttää Scalan min-funktiota (luku 1.6) määrittääksesi, paljonko pisteitä voidaan vähentää. Vaihtoehtoinen ratkaisutapa hyödyntää max-funktiota. Myös muita toimivia ratkaisutapoja on, ja nekin ovat sallittuja.

Testaa funktiotasi, jotta olet varma sen toimivuudesta!

Vaihe 2/2: paluuarvo kuntoon

Tiedämme, että Scalassa funktion viimeinen suoritettava käsky määrää, mitä funktio palauttaa. Ensimmäinen yritys algoritmin laatimiseksi voisi siis olla seuraava.

  1. Vähennä pisteitä korkeintaan ykköseen asti.

  2. Laske ja palauta onnistuneesti vähennetty määrä.

Ongelma on se, että paluuarvon laskemiseen tarvittaisiin sekä kolmannen parametrin arvo (sakon koko) että pelaajan alkuperäinen pistemäärä. Kun vähennys on jo tehty, ei alkuperäinen pistemäärä ole enää missään tallessa eikä kakkosvaiheessa saada paluuarvoa laskettua mitenkään.

Toinen yritys.

  1. Laske, paljonko voidaan vähentää, ja pistä tuo määrä talteen.

  2. Vähennä pisteitä tuon verran.

  3. Palauta ykköskohdassa talteen pantu määrä.

Tämä versio on parempi, koska siinä palautettava arvo lasketaan jo ennen sakon toimeenpanoa.

Algoritmin toteuttamiseksi tarvitsemme Scala-käskyn, jolla saamme ykköskohdassa vähennettävän määrän talteen. Sehän käy: tähän sopii paikallinen muuttuja.

Vinkkejä

Muista, että monesta rivistä koostuvakin funktio palauttaa viimeisenä evaluoidun lausekkeen arvon. Tällaiseksi lausekkeeksi kelpaa vain vaikka pelkkä muuttujan nimikin.

Jos haluat, voit katsoa seuraavan animaation, joka esittelee erään toimivan ratkaisuperiaatteen tälle tehtävälle. (Ohjelmakoodi ei animaatiossa näy. Sen joudut kirjoittamaan itse).

Huomaa animaatiosta myös, että sakko-funktiolle välitetään parametriksi viittaus puskuriin eikä kopiota puskurista sisältöineen. Juuri tämän vuoksi puskuriin tehty muutos voidaan havaita samaan puskuriin osoittavan viittauksen kautta, myös funktion suorittamisen jälkeen.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Parametrittomia funktioita

Kaikilla toistaiseksi näkemillämme funktioilla on ollut vähintään yksi parametri. Funktio voi kuitenkin olla myös parametriton.

def yksPlusYks = 1 + 1

Ei kaarisulkeita eikä parametriluetteloa.

Tuo funktio palauttaa aina kutsuttaessa luvun kaksi:

yksPlusYksres15: Int = 2
yksPlusYksres16: Int = 2

Parametriton funktiomäärittely näyttää paljon muuttujan määrittelyltä. Vertaa:

def funktio  = sqrt(500)
val muuttuja = sqrt(1000)

Täydennetään tuota koodia vielä neljällä lisärivillä:

println(funktio)
println(funktio + funktio)
println(muuttuja)
println(muuttuja + muuttuja)

Mieti, mitä tapahtuu, kun kone suorittaa tuon koodin. Mitkä seuraavista väitteistä pitävät paikkansa? Voit myös kokeilla REPLissä.

Erilaisista virheistä

Kauanko aikaa meni funktion kirjoittamiseen?
Ei kauaa.
Kauanko aikaa meni virheiden korjaamiseen?
Kauan.

Yhdeksänkymmentä prosenttia ajastasi kuluu virheiden etsimiseen siitä kymmenestä prosentista koodia, jonka viimeksi olet kirjoittanut.

—alkuperä tuntematon

Virheiden etsiminen vie huomattavan osan ohjelmoijan ajasta. Nyt kun olet päässyt kirjoittamaan ohjelmakoodia, on hyvä tuntea ohjelmissa esiintyvien virheiden päätyypit.

Käännösaikaiset virheet

Käännösaikaiset virheet (compile-time error) havaitaan automaattisesti jo ennen ohjelman ajamista. Niiden nimi tulee siitä, että ne voi paikallistaa kääntäjäksi kutsutulla työkalulla, jollaisella mm. Scala-koodi muutetaan paremmin tietokoneen suoritettavaksi sopivaan muotoon (mistä lisää luvussa 5.4).

Esimerkiksi väärä välimerkkien käyttö koodissa ja (Scalassa) vääräntyyppisen arvon sijoittaminen muuttujaan tuottavat käännösaikaisia virheilmoituksia. Monet käännösaikaiset virheet ovatkin juuri syntaksivirheitä (syntax error), jotka johtuvat siitä, ettei kirjoitettu koodi vastaa ohjelmointikielen syntaksin (kieliopin) sääntöjä.

Kokeneelle ohjelmoijalle useimpien käännösaikaisten virheiden korjaaminen on vaivatonta, ja aloittelijallekin tämä on yleensä helpoin virhetyyppi.

Luvussa 1.7 oli puhetta IntelliJ’n Build-lehdelle ilmestyvistä virheilmoituksista ja editorissa punaisella merkityistä virheistä. Nämä voi kaikki lukea juuri käännösaikaisiksi virheiksi (joskin IntelliJ osaa ilmoittaa ison osan niistä välittömästi jo koodia editoidessa).

Ajonaikaiset virheet

Ajonaikaiset virheet (runtime error) ovat ohjelmoijan kannalta keljumpia. Ne ilmenevät vasta ohjelmaa ajaessa ja saattavat ilmetä vain joillakin syötearvoilla.

Klassinen esimerkki ajonaikaisesta virheestä on nollalla jakaminen: jos jakajana käytetään nollaksi evaluoituvaa lauseketta, syntyy ajonaikainen virhetilanne, joka voi "kaataa" ohjelman (eli keskeyttää äkisti sen suorituksen), ellei ohjelmoija ole tällaiseen tapaukseen erikseen varautunut.

Toinen esimerkki on indeksointivirheet. Niitä esiintyi muun muassa siinä luvun 1.5 pikkutehtävässä, jossa kokeilit liian suuria ja pieniä indeksejä puskuria käyttäessäsi.

Monista ajonaikaisista virheistä käytetään nimitystä poikkeus (exception).

Ajonaikaisiin virheisiin ja ohjelmien kaatumisiin palataan mm. luvussa 4.2.

Koska REPLissä koodia ajetaan saman tien kuin se kirjoitetaan, ero käännösaikaisten ja ajonaikaisten virheilmoitusten välillä hämärtyy siellä työskennellessä.

Loogiset virheet

Loogisiksi virheiksi (logical error) sanotaan sellaisia virhetilanteita, joissa luotu ohjelma kyllä teknisesti "toimii" mutta tekee jotakin muuta kuin oli tarkoitus, ehkä jotain täysin hyödytöntä. Esimerkki tästä on väärän laskutoimituksen suorittaminen.

Jotkin loogiset virheet on helppo huomata koodia tai ohjelman toimintaa tutkimalla. Toiset ovat vaikeampia. Joka tapauksessa loogisten virheiden paikantaminen on ohjelmoijan vastuulla, koska tietokone ei niistä osaa varoittaa.

Tietotyypit ja Scala

Tarkastellaan kierroksen lopuksi hieman sitä, miten tietotyyppejä käytetään Scala-ohjelmissa ja miten tämä on jo ilmennyt kirjoittamassamme koodissa. Seuraava harmaareunuksinen laatikko taustoittaa aihetta, mutta ei ole kurssin kannalta välttämätön. Voit ohittaa sen, jos on kiire. Laatikon alla on välttämättömämpää asiaa.

Tyyppijärjestelmistä

Ohjelmointikielen luonteeseen vaikuttaa merkittävästi sen tyyppijärjestelmä (type system) eli ne yleiset säännöt, jotka määräävät ohjelmien osien tietotyypit sekä sen, miten nuo tyypit vaikuttavat kielen käyttöön. Tyyppijärjestelmien yksityiskohdat tai teoria eivät kuulu tämän kurssin sisältöön, mutta voimme silti käsitellä eräitä yleissivistäviä perusasioita.

Tyyppiturvallisuus

Scala on hyvin tyyppiturvallinen (type safe) kieli. Kullakin Scala-kielen arvolla on tietty tietotyyppi, ja tuo tyyppi rajoittaa, mitä arvolla voi tehdä. Kokonaisluvuilla voi suorittaa laskutoimituksia, merkkijonoja voi yhdistellä ja puskureihin voi lisätä uusia alkioita; toisaalta kokonaislukuun ei voi lisätä alkioita, ja yrityksestä tehdä näin seuraa asianmukainen virheilmoitus.

Klassinen esimerkki ohjelmointikielestä, jonka tyyppijärjestelmä on suhteellisen epäturvallinen, on kieli nimeltä C. Tätä kieltä käyttävä ohjelmoija voi antaa "tyyppien vastaisia" käskyjä, joilla on yhteydestä riippuvia ja joissain tapauksissa arvaamaattomiakin seurauksia. Tämä tarjoaa joitakin lisämahdollisuuksia ohjelmoijalle, mutta kasvattaa ohjelmointivirheiden mahdollisuutta hidastaen (osaavaakin) ohjelmoijaa ja kenties vahingoittaen lopputulosta.

Staattinen vs. dynaaminen tyypitys

Luvussa 1.2 todettiin, että ohjelmilla on kaksi "olomuotoa": staattinen ja dynaaminen. Asian voi todeta kurssimateriaaliin upotetuista animaatioista.

Jako näkyy myös tyyppijärjestelmissä.

Scala on staattisesti tyypitetty (statically typed), eli Scala-ohjelman osien tyypit on hyvin määritelty jo ohjelman staattisessa olomuodossa, siis ohjelmakoodissa. Esimerkiksi kullakin Scala-ohjelman muuttujalla ja lausekkeella on tietty tietotyyppi, joka voidaan päätellä koodia tarkastelemalla. Tämä mahdollistaa muun muassa sen, että jo ennen kuin ajamme ohjelman, käyttämämme työkalut voivat varoittaa virheellisistä käskyistä (esimerkiksi jos yrität käyttää parametriarvona kokonaislukua, kun pitäisi käyttää merkkijonoa). Staattinen tyypitys voi myös tuottaa tehokkaampia (nopeammin suoritettavia) ohjelmia ja muutenkin tukea ohjelmointityökalujen toimintaa. Staattisen tyypityksen muut edut korostuvat isommissa ohjelmissa, joissa laatu ja luotettavuus ovat tärkeitä kriteereitä.

(Vääräntyyppisen arvon käyttäminen esimerkiksi funktion parametriarvona voi kuulostaa virheenä tyhmältä, mutta se on huomattavasti yleisempää kuin äkkiä arvaisikaan — myös ammattilaisten laatimissa ohjelmissa. Tulet mitä luultavimmin huomaamaan tämän kurssin aikana myös itse.)

Dynaamisesti tyypitetyssä (dynamically typed) kielessä ohjelmakoodin osilla ei suoranaisesti ole tietotyyppejä. Esimerkiksi Python-kielessä muuttujilla ei ole tyyppejä, vaan muuttujiin voi sijoittaa mitä tahansa arvoja, ja tiettyyn muuttujaan sijoitetun arvon tyyppi voi määräytyä vasta ohjelman ajon aikana esimerkiksi käyttäjän antaman syötteen perusteella. Tämä tekee koodin kirjoittamisesta jonkin verran joustavampaa ja osin nopeampaa; myös itse ohjelmointikieli voi olla staattisesti tyypitettyä kieltä yksinkertaisempi. Eräitä dynaamisen tyypityksen haittapuolia ovat suurempi ohjelmointivirheiden mahdollisuus sekä se, että suurempi osa virheistä havaitaan vasta ajamalla ohjelmaa eri syötearvoilla.

Eri ohjelmoijilla on toisistaan voimakkaastikin poikkeavia näkemyksiä siitä, onko staattinen vai dynaaminen tyypitys parempi ratkaisu. Tämä on ollut monen uskonsodan, sivistyneen keskustelun ja vähäisemmän kinan aihe.

Tyyppimerkinnöistä

Monissa staattisesti tyypitetyissä kielissä (esim. Java) tyyppimerkintöjä kirjoitetaan ohjelmakoodiin runsaasti. Tällaisissa kielissä muuttujaa määrittelevään koodiin on aina tai usein kirjoitettava muuttujan tyyppi, ja kaikki tai useimmat paluuarvojen tyypit on nekin yleensä erikseen mainittava. Toisaalta dynaamisesti tyypitetyssä koodissa, kuten (perus-)Pythonissa, tällaisia merkintöjä ei kirjoiteta lainkaan.

Scala on staattisesti tyypitetty, mutta emme ole juurikaan tyyppimerkintöjä koodiin kirjoittaneet. Kiinnitimme aiheeseen ensimmäistä kertaa kunnolla huomiota vasta äsken luvussa 1.7, kun määrittelimme funktioille parametrimuuttujia. (Pieni esimaku oli myös luvussa 1.5, jossa tyhjän puskurin määrittelyyn piti kirjata tyyppi.) Olemme voineet jättää asian vähälle huomiolle, koska Scala-kieleen liittyvien sääntöjen nojalla hyvin monet tyyppitiedot voidaan päätellä automaattisesti. Tämän vuoksi monien asioiden tekeminen Scala-kielellä sujuu käytännössä yhtä näppärästi kuin dynaamisesti tyypitetyllä kielellä.

Lisää Scalan tyyppimerkinnöistä alla.

Tyyppipäättely Scalassa

Scala-koodissa on tiettyjä kohtia, joihin täytyy kirjoittaa tyyppimerkinnät. Funktioiden parametrimuuttujat ovat tällainen kohta; niille kirjaamme tyypit kaksoispisteen perään. Myös muilla Scala-koodin osilla kuin parametrimuuttujilla on tietotyyppejä, mutta nämä tyypit selviävät useimmissa tilanteissa automaattisesti Scala-työkaluihin rakennetun tyyppipäättelyn (type inference) avulla.

Olemme esimerkiksi määritelleet muuttujia näin:

val luku = 123
val teksti = "Luku on " + luku + "."

Nuo käskyt ovat itse asiassa lyhennyksiä näistä:

val luku: Int = 123
val teksti: String = "Luku on " + luku + "."

Lyhyemmät muodot toimivat, koska muuttujien tyypit voidaan päätellä niihin sijoitettavista arvoista. On täysin sallittua merkitä tyypit myös näille val-muuttujille — kuten tuossa yllä — mutta se on tarpeetonta, eikä sitä yleensä tehdä.

Myös alla toistetussa tutussa funktiomäärittelyssä olemme itse asiassa jättäneet erään tietotyypin automaattisesti pääteltäväksi:

def keskiarvo(eka: Double, toka: Double) = (eka + toka) / 2

Tuo koodi on lyhennys seuraavasta:

def keskiarvo(eka: Double, toka: Double): Double = (eka + toka) / 2

Viimeinen Double-merkintä sulkeiden perässä on funktion paluuarvon tietotyyppi: tämä funktio ottaa parametreikseen kaksi desimaalilukua ja myös palauttaa desimaaliluvun. Paluuarvon tyyppi on kuitenkin automaattisesti pääteltävissä laskutoimituksesta (eka + toka) / 2, koska tunnetaan parametrien tyypit. Niinpä voimme jättää sen kirjoittamatta.

Myöhemmin kurssilla kohtaamme myös muita tilanteita kuin parametrimuuttujat, joissa tyyppejä on kirjoitettava Scala-koodiin erikseen.

Siitä virheestä vielä

Muistatko virheen, joka oli annetussa sanallinenArvosana-pohjakoodissa? Siis tässä:

def sanallinenArvosana(tehtavaarvosana: Int, tenttibonus: Int, aktiivisuusbonus: Int) 
  val kuvaukset = Buffer("hylätty", "välttävä", "tyydyttävä", "hyvä", "erittäin hyvä", "erinomainen")
  // ...

IntelliJ’n editori korostaa def-rivin lopun. Virheilmoitus motkottaa: Missing return type. Niinhän se on, että tuohon voisi kirjoittaa paluuarvon tyypinkin, mutta varsinainen ongelma on puuttuva yhtäsuuruusmerkki.

Yleisempi oppi tästä on se, että kone osaa useammin havaita, että jokin on pielessä, kuin se osaa osuvasti kuvata pielessäolon syyn ja mitä asialle kannattaisi tehdä. Osa virheilmoituksista on kyllä osuviakin, mutta virheen tulkinta ja korjaus ovat viime kädessä ohjelmoijan vastuulla.

Yhteenvetoa

  • Funktio voi kutsua toista funktiota.

    • Tällöin kutsupinon päälle syntyy uusi kehys, ja kutsuva funktio jää odottamaan kutsutun funktion suorituksen päättymistä. Vain kutsupinon päällimmäinen kehys on aktiivisessa käytössä.

    • Voidaan siis laatia funktioita, joita hyödynnetään toisissa funktioissa jonkin osatehtävän suorittamiseen.

  • Ohjelmissa voi esiintyä käännösaikaisia, ajonaikaisia ja loogisia virheitä.

  • Scala-koodiin pitää tiettyihin kohtiin kirjoittaa tyyppimerkintöjä. Ilmeinen esimerkki tästä ovat parametrimuuttujat. Suuren osan tietotyypeistä Scala-työkalut osaavat kuitenkin päätellä automaattisesti.

  • Lukuun liittyviä termejä sanastosivulla: funktio, funktiokutsu, kutsupino, kehys, paikallinen muuttuja; virhe; tyyppimerkintä, tyyppipäättely.

Mitä nyt ja mitä seuraavaksi?

Osaat nyt kirjoittaa itse omia tietokoneohjelmien osia — funktioita — ja käyttää niiden rakennusmateriaalina erilaisia ohjelmointitekniikoita kuten lukuja, puskureita ja toisia funktioita. Olemme silti vielä vähän matkan päässä siitä, miten laaditaan ohjelmakokonaisuus, jossa on lukuisia yhdessä toimivia rakennusosia.

Ohjelmakokonaisuuksien rakentamiseen on olemassa erilaisia tapoja. Tällä kurssilla tutustumme seuraavasta kierroksesta alkaen yhteen hyvään tapaan.

Itsenäisestä työskentelystä

Nyt kun sinulla on jo vähän tuntumaa ohjelmointiin, on suotuisa hetki palata kurssin työskentelykäytäntöihin, joista jo luvussa 1.1 mainittiin. Kurssilla saat siis tehdä tehtävät yksin tai parin kanssa yhteistyössä (ei tehtävät parin kanssa jakaen).

Pariin kuulumattomienkin opiskelijoiden kanssa voit keskustella tehtävistä. Se onkin kannustettavaa.

Ohjelmakoodi pitää kunkin opiskelijan tai parin laatia itse. Kummankin parityötä tekevän opiskelijan on ymmärrettävä kaikki parin palauttama koodi. On ehdottomasti kiellettyä kopioida ratkaisuja toisilta tai toisille tai levittää niitä verkossa. Tämä koskee myös kurssilla käsiteltyjä esimerkkiratkaisuja.

Tehtävien ratkaiseminen niin sanotuilla tekoälytyökaluilla (ChatGPT, GitHub Copilot, JetBrains AI, Bing tms.) on kielletty. Tällaisten työkalujen tuottamien ratkaisujen palauttaminen on vilppiä samoin kuin toiselta ihmiseltä kopioitujenkin. Kurssin tarjoamaa materiaalia ei saa syöttää "tekoälytyökaluihin" tai muihin ulkoisten toimijoiden palveluihin.

Vilppitapaukset johtavat opintosuoritusten hylkäämiseen, ja asiasta välitetään tieto koulutusohjelmalle yleisen vilppitapausohjeen mukaisesti.

Seuraava kuittaus on kurssin jatkon kannalta pakollinen. Jos asiassa on epäselvää, keskustele siitä assarien tai opettajan kanssa.

Palaute

Huomaathan, että tämä on henkilökohtainen osio! Vaikka olisit tehnyt lukuun liittyvät tehtävät parin kanssa, täytä palautelomake itse.

Tekijät

Tämän oppimateriaalin kehitystyössä on käytetty apuna tuhansilta opiskelijoilta kerättyä palautetta. Kiitos!

Materiaalin luvut tehtävineen ja viikkokoosteineen on laatinut Juha Sorva.

Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.

Tehtävien automaattisen arvioinnin ovat toteuttaneet: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Joel Toppinen, Anna Valldeoriola Cardó ja Aleksi Vartiainen.

Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.

Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista suunnittelivat Juha Sorva ja Teemu Sirkiä. Teemu Sirkiä ja Riku Autio toteuttivat ne apunaan Teemun aiemmin rakentamat työkalut Jsvee ja Kelmu.

Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset laati Juha Sorva.

O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen, Juha Sorva ja Jaakko Nakaza. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.

Tapa, jolla käytämme O1Libraryn työkaluja (kuten Pic) yksinkertaiseen graafiseen ohjelmointiin, on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.

Oppimisalusta A+ luotiin alun perin Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Nykyään tätä avoimen lähdekoodin projektia kehittää Tietotekniikan laitoksen opetusteknologiatiimi ja tarjoaa palveluna laitoksen IT-tuki; sitä ovat kehittäneet kymmenet Aallon opiskelijat ja muut.

A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen suunnitteluun ja toteutukseen on osallistunut useita opiskelijoita yhteistyössä O1-kurssin opettajien kanssa.

Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.

Lisäkiitokset tähän lukuun

Inspiraationa muuttujan käyttöaluetta koskevalle kysymykselle oli eräs Kathi Fislerin, Shriram Krishnamurthin ja Preston Tunnell Wilsonin kirjoittama artikkeli.

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