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

Kurssin viimeisimmän version löydät täältä: O1: 2024

Luku 1.8: Funktioista, tyypeistä ja virheistä

Tästä sivusta:

Pääkysymyksiä: Miten laadin hieman jännempiä funktioita? Miksi juuri parametrimuuttujille kirjoitetaan tietotyypit koodiin?

Mitä käsitellään? Oman funktion kutsuminen toisesta funktiosta; lisää kutsupinon merkityksestä. Lisäharjoitusta ohjelmoinnin käytännöstä. Puskurin käsittely funktiossa. Erilaisia ohjelmavirheitä. Tyyppimäärittelyt ja Scalan tyyppipäättely.

Mitä tehdään? Enimmäkseen ohjelmoidaan. Luettavaakin on.

Suuntaa antava työläysarvio:? Tähän mennessä työläin ja vaativin luku, erityisesti jos ohjelmointi on sinulle uutta. Ajankäyttö vaihdellee paljon. Monet selviävät varmasti parissa, kolmessa tunnissa ainakin assarin avulla.

Pistearvo: A130.

Oheisprojektit: Aliohjelmia.

../_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)
}
Voidaan ajatella, että funktiot voivat olla kahdella eri tavalla "sisäkkäin". Ensinnäkin 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.

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

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, palautusarvoista 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? (Huom. 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?

Etenkin jos tehtävä tuntuu vaikealta, kannattaa miettiä, millaisia kehyksiä koodia 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 = a
}
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 aliohjelmia.scala kaksi vaikutuksetonta funktiota, joilla voidaan 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 aaltosulkea tuo runko.
    • Muista tällöin myös sisennykset, jotka korostavat, mitkä rivit kuuluvat funktion toteutukseen.
    • 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.

Palauttaminen

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 aliohjelmia.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.8661417322834666)
jaloiksiJaTuumiksi(0.0254)res9: (Double, Double) = (0.0,1.0)

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

Palaamme parien pariin luvussa 8.4.

Ohjelmointitehtävä: sanallinenArvosana

Tehtävänanto

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

Täydennä funktio toimivaksi:

  • Funktion tulee ottaa parametreikseen tehtäväarvosana ja tentti- ja aktiivisuusbonukset aivan kuten luvussa 1.7 tehty kurssiarvosana-funktiokin teki.
  • Kokonaisluvun sijaan funktion tulee palauttaa merkkijono, joka kuvaa kyseistä arvosanaa: esim. kakkonen on "tyydyttävä" ja viitonen "erinomainen".
  • Annetussa pohjakoodissa on pieni mutta vakava virhe! Korjaa se.

Pohjakoodi löytyy myös aliohjelmia.scala-tiedostosta. Täydennä puuttuva osa sinne.

Ohjeita ja vinkkejä

  • Käytä apuna koodissa jo määriteltyä puskuria sekä luvussa 1.7 laatimaasi kurssiarvosana-funktiota. Nämä yhdistämällä toteutus on hyvinkin suoraviivainen.
  • Muistithan myös korjata virheen annetusta pohjakoodista? Kannattaa kokeilla myös, miten funktiosi käyttäytyy, jos virhettä ei korjata. Näin asia jää paremmin mieleen, ja todennäköisemmin vältät tämän kiusallisen virheen jatkossa vapaammin ohjelmoidessasi.

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

Ohjelmointitehtävä: tuplaaPisteet

Tämän luvun viimeiset kaksi funktiota ovat vaikutuksellisia: ne muuttavat parametriksi annetun puskurin sisältöä. (Vrt. 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 aliohjelmia.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öä siten, että kyseisen pelaajan pisteet kaksinkertaistuvat.

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 tämän kurssin tehtävissä ei tarvitse välittää mistään tällaisista 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.
  • Yksi puskurin indeksille uuden arvon sijoittava käsky riittää funktion rungoksi.

Palauttaminen

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

Ohjelmointitehtävä: sakko

Jatketaan saman kuvitteellisen pelin parissa. Nyt laadittavaksi tulee 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ää.
  • 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, jossa oletetaan, että tarvittavat import-käskyt on jo annettu.

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. Palautusarvo kertoo, että kolme pistettä on saatu vähennettyä.

Vähennys voidaan havaita 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 palautusarvoa

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: palautusarvo 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 kuitenkin se, että palautusarvon 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 palautusarvoa 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 laitettu määrä.

Tämä versio on parempi, koska siinä palautettava arvo lasketaan jo ennen sakon toimeenpanoa. Saamme algoritmin kuitenkin toteutettua vain, jos ykköskohdassa saadaan vähennettävä määrä jotenkin talteen. Ja saadaanhan se: tähän voi käyttää paikallista muuttujaa.

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 (mutta jossa ohjelmakoodi ei näy; sen joudut kirjoittamaan itse).

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

Palauttaminen

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

Erilaisista virheistä

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) voidaan havaita automaattisesti jo ennen ohjelman ajamista. Niiden nimi tulee siitä, että ne voidaan paikallistaa kääntäjätyökalulla, jollaisella mm. Scala-koodi muutetaan paremmin tietokoneen suoritettavaksi sopivaan muotoon (mistä lisää luvussa 5.2).

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.

Kun luvussa 1.7 käsiteltiin Eclipsen tapaa ilmoittaa virheistä punaisella värillä, oli kyse juuri käännösaikaisista virheistä.

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 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 ovat indeksointivirheet, joita 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.1.

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 mitä oli tarkoitus, mahdollisesti 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 pohjustaa 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 tietotyypit vaikuttavat kielen käyttöön. Tyyppijärjestelmien yksityiskohdat tai teoria eivät kuulu tämän kurssin sisältöön, mutta voimme kuitenkin 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 hyvin arvaamaattomiakin seurauksia. Tämä tarjoaa joitakin lisämahdollisuuksia ohjelmoijalle, mutta kasvattaa ohjelmointivirheiden mahdollisuutta hidastaen (osaavaakin) ohjelmoijaa ja mahdollisesti 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 ilmeisellä tavalla virheellisistä käskyistä (esimerkiksi jos yritämme käyttää parametriarvona kokonaislukua, kun pitäisi käyttää merkkijonoa). Staattinen tyypitys voi myös tuottaa tehokkaampia (nopeammin suoritettavia) ohjelmia ja muutenkin mahdollistaa parempia apuohjelmia ohjelmoijalle.

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

Dynaamisesti tyypitetyssä (dynamically typed) ohjelmointikielessä 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.

Tyyppimäärittelyistä

Monissa staattisesti tyypitetyissä kielissä (esim. Java) tyyppimäärittelyjä kirjoitetaan ohjelmakoodiin hyvin runsaasti. Esimerkiksi muuttujaa määriteltäessä on aina kirjoitettava muuttujan tyyppi ohjelmakoodiin, ja kaikki parametrimuuttujien ja palautusarvojen tyypit on aina erikseen mainittava. Toisaalta dynaamisesti tyypitetyssä kielessä kuten Python tällaisia määrittelyjä ei kirjoiteta koodiin lainkaan.

Scala on staattisesti tyypitetty, mutta emme ole juurikaan tyyppimäärittelyjä 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 tyyppimäärittelyistä alla.

Tyyppipäättely Scalassa

Scala-koodissa on tiettyjä kohtia, joihin täytyy kirjoittaa tyyppimäärittelyjä. 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 kirjoittaa tyyppimäärittelyt 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ä sulkujen perässä on funktion palautusarvon tietotyyppi: tämä funktio ottaa parametreikseen kaksi desimaalilukua ja myös palauttaa desimaaliluvun. Palautusarvon 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.

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 tyyppimäärittelyjä. 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; tyyppimäärittely, 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 kuitenkin 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ä perusperiaatteiden tasolla tai johonkin yksityiskohtaan liittyen. Se onkin kannustettavaa.

Ohjelmakoodi pitää kunkin opiskelijan tai parin laatia itse. On ehdottomasti kiellettyä kopioida ratkaisuja toisilta tai toisille.

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!

Kierrokset 1–13 ja niihin liittyvät tehtävät ja viikkokoosteet on laatinut Juha Sorva.

Kierrokset 14–20 on laatinut Otto Seppälä. Ne eivät ole julki syksyllä, mutta julkaistaan ennen kuin määräajat lähestyvät.

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

Tehtävien automaattisen arvioinnin ovat toteuttaneet Riku Autio, Jaakko Kantojärvi, Teemu Lehtinen, Timi Seppälä, Teemu Sirkiä ja Aleksi Vartiainen.

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

Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista ovat suunnitelleet Juha Sorva ja Teemu Sirkiä. Niiden teknisen toteutuksen ovat tehneet Teemu Sirkiä ja Riku Autio käyttäen Teemun toteuttamia Jsvee- ja Kelmu-työkaluja.

Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset on laatinut Juha Sorva.

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

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

Oppimisalusta A+ on luotu Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Pääkehittäjänä toimii tällä hetkellä Jaakko Kantojärvi, jonka lisäksi järjestelmää kehittävät useat tietotekniikan ja informaatioverkostojen opiskelijat.

Kurssin tämänhetkinen henkilökunta on kerrottu luvussa 1.1.

Lisäkiitokset tähän lukuun

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

../_images/imho1.png
Palautusta lähetetään...