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, varsinkin jos ohjelmointi on sinulle uutta. Ajankäyttö vaihdellee paljon. Monet selviävät varmasti kolmessa tunnissa ainakin assarin avulla, mutta enemmänkin voi mennä.
Pistearvo: A130.
Oheismoduulit: Aliohjelmia.
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)
}
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.
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
- Laadi
liigapisteet
-funktio. Noudata samoja työvaiheita kuin edellisen luvun 1.7 ohjelmointitehtävissä. Älä unohda testata, että funktiosi toimii ennen kuin jatkat. - 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
-funktiotajoukkueenTiedot
-funktion sisältä laskeaksesi joukkueen pisteet. - Koodista tulee luettavampi, jos käytät paikallista muuttujaa apuna pelien kokonaislukumäärän tallentamiseen.
- 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 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.
}
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
aliohjelmia.scala
an. - Annetussa pohjakoodissa on pieni mutta vakava virhe! Korjaa se.
- Täydennä funktion runko.
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? Voit myös kokeilla, 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
Seuraava funktio on vaikutuksellinen: se muuttaa 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öä 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 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.
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ä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)
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.
- Vähennä pisteitä korkeintaan ykköseen asti.
- 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.
- Laske, paljonko voidaan vähentää, ja pistä tuo määrä talteen.
- Vähennä pisteitä tuon verran.
- 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.
if
-käsky?
Jos olet ohjelmoinut ennen tätä kurssia, olet ehkä törmännyt
if
-valintakäskyyn ja mietit löytyisikö Scalasta vastaavaa ja
pitäisikö sitä käyttää tässä.
Scalastakin if
-niminen käsky löytyy, ja aikanaan se tälläkin
kurssilla tulee vastaan. Et kuitenkaan tarvitse sitä tässä.
Tehtävään löytyy siisti (ja opettavainen) ratkaisu ilmankin.
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).
Huomma 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
Tuo funktio palauttaa aina kutsuttaessa luvun kaksi:
yksPlusYksres15: Int = 2 yksPlusYksres16: Int = 2
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.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 Messages-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 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.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 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 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 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ä sulkeiden 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ä
Palaute
Huomaathan, että tämä on henkilökohtainen osio! Vaikka olisit tehnyt lukuun liittyvät tehtävät parin kanssa, täytä palautelomake itse.
Tekijät
Tämän oppimateriaalin kehitystyössä on käytetty apuna tuhansilta opiskelijoilta kerättyä palautetta. Kiitos!
Materiaalin luvut tehtävineen ja viikkokoosteineen on laatinut Juha Sorva.
Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.
Tehtävien automaattisen arvioinnin ovat toteuttaneet: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä ja Aleksi Vartiainen.
Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.
Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista suunnittelivat Juha Sorva ja Teemu Sirkiä. Teemu Sirkiä ja Riku Autio toteuttivat ne apunaan Teemun aiemmin rakentamat työkalut Jsvee- ja Kelmu.
Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset laati Juha Sorva.
O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.
Tapa, jolla käytämme O1Libraryn työkaluja (kuten Pic
) yksinkertaiseen graafiseen
ohjelmointiin, on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi
oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.
Oppimisalusta A+ luotiin alun perin Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Nykyään tätä avoimen lähdekoodin projektia kehittää Tietotekniikan laitoksen opetusteknologiatiimi ja tarjoaa palveluna laitoksen IT-tuki. Pääkehittäjänä on tällä hetkellä Markku Riekkinen, jonka lisäksi A+:aa ovat kehittäneet kymmenet Aallon opiskelijat ja muut.
A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen ovat luoneet Nikolai Denissov, Olli Kiljunen, Nikolas Drosdek, Styliani Tsovou, Jaakko Närhi ja Paweł Stróżański yhteistyössä Juha Sorvan, Otto Seppälän, Arto Hellaksen ja muiden kanssa.
Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.
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.