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 kolmessa tunnissa ainakin assarin avulla, mutta enemmänkin voi mennä.
Pistearvo: A130.
Oheisprojektit: 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.
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
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öä 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 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 olevan positiivinen.
- 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ä import
on jo hoidettu.
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.
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.
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.
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.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 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ä
Palaute
Huomaathan, että tämä on henkilökohtainen osio! Vaikka olisit tehnyt lukuun liittyvät tehtävät parin kanssa, täytä palautelomake itse.
Tekijät
Tämän oppimateriaalin kehitystyössä on käytetty apuna tuhansilta opiskelijoilta kerättyä palautetta. Kiitos!
Kierrokset 1–13 ja niihin liittyvät tehtävät ja viikkokoosteet on laatinut Juha Sorva.
Kierrokset 14–20 on laatinut Otto Seppälä. Ne eivät ole julki syksyllä, mutta julkaistaan ennen kuin määräajat lähestyvät.
Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.
Tehtävien automaattisen arvioinnin ovat toteuttaneet: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä ja Aleksi Vartiainen.
Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.
Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista ovat suunnitelleet Juha Sorva ja Teemu Sirkiä. Niiden teknisen toteutuksen ovat tehneet Teemu Sirkiä ja Riku Autio käyttäen Teemun toteuttamia Jsvee- ja Kelmu-työkaluja.
Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset on laatinut Juha Sorva.
O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.
Opetustapa, jossa käytämme O1Libraryn työkaluja (kuten Pic
) yksinkertaiseen graafiseen
ohjelmointiin on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi
oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.
Oppimisalusta A+ on luotu Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Pääkehittäjänä toimii tällä hetkellä Jaakko Kantojärvi, jonka lisäksi järjestelmää kehittävät useat tietotekniikan ja informaatioverkostojen opiskelijat.
Kurssin tämänhetkinen henkilökunta on kerrottu luvussa 1.1.
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.