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 virheitä ohjelmissa. 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)
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.
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, 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 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
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 sisennykset.
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.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
aliohjelmia.scala
an.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 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 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. Palautusarvo 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 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 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.
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
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) 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 ovat 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 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. Staattisen tyypityksen muut edut korostuvat voimakkaimmin isommissa ohjelmissa, joissa laatu ja luotettavuus ovat tärkeitä kriteereitä.
(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) 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.
Tyyppimäärittelyistä
Monissa staattisesti tyypitetyissä kielissä (esim. Java) tyyppimäärittelyjä kirjoitetaan ohjelmakoodiin runsaasti. Tällaisissa kielissä muuttujaa määrittelevään koodiin on aina tai usein kirjoitettava muuttujan tyyppi, ja kaikki tai useimmat palautusarvojen tyypit on nekin yleensä erikseen mainittava. Toisaalta dynaamisesti tyypitetyssä koodissa, kuten (perus-)Pythonissa, tällaisia määrittelyjä ei kirjoiteta 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.
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 palautusarvon 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 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 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ä
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, Antti Immonen, Jaakko Kantojärvi, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, 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 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 nyt 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 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.
Funktiot voivat olla eri tavoin "sisäkkäin". Ensinnäkin funktiokutsulausekkeet voivat olla ohjelmakoodissa sisäkkäin.