Kurssin viimeisimmän version löydät täältä: O1: 2024
Luku 4.3: Olemattomia arvoja
Tästä sivusta:
Pääkysymyksiä: Miten ilmaisen ohjelmassa, että jotakin tietoa ei
välttämättä ole? Mitä voin käyttää null
-arvojen sijaan?
Mitä käsitellään? null
-arvon ongelma. Option
-tietotyypin
peruskäyttö. match
-valintakäsky.
Mitä tehdään? Luetaan. Pieniä tehtäviä. (Isompia ohjelmointitehtäviä tämän luvun aiheesta on seuraavassa luvussa.)
Suuntaa antava työläysarvio:? Tunti tai puolitoista.
Riippuu ihmettelyn määrästä. Luvun pääaihe eli Option
-tyyppi on
aluksi monelle haastava, ja match
-käskyn muotoilussa on vähän
totuttelemista.
Pistearvo: A15.
Oheismoduulit: GoodStuff.
Jatkoa viime numerosta
Jäimme tilanteeseen, jossa ohjelma kaatui, koska yritimme katsoa viittauksen kautta
kokemusolion arvosanaa — another.rating
— mutta another
-muuttujan arvona olikin
null
eli ei viittausta mihinkään. Tämä siksi, että ensimmäistä kokemusoliota lisättäessä
kategorialla ei vielä ole suosikkia, minkä merkitsemiseen käytimme null
-arvoa.
Tietokone ei osaa itse päätellä, miten sen pitäisi suoriutua tapauksesta, jossa edellistä suosikkikokemusta ei ole. Ohjelmoijan pitää tämäkin erikseen kirjata koodiin. Viimelukuisessa algoritmissamme emme huomioineet lainkaan tätä mahdollisuutta.
Tässä kaksi erilaista mahdollisuutta korjata tilanne:
- Käytetään jotakin toista tietotyyppiä, jonka avulla voidaan kuvata sitä, että "suosikilla on nolla tai yksi arvoa".
- Käytetään olemattoman olion merkkinä
null
-arvoa, jonka voi sijoittaa (melkein) minkä vain tyyppiseen muuttujaan. Kuitenkin muistetaan varmistaa muilla käskyillä, ettei yritetä käsitellä olemattoman olion piirteitä.
On perusteltavissa, että ensimmäinen vaihtoehto on usein parempi. (Perustelut alempana.)
Kohta tarkastelemme erästä tällaista ratkaisutapaa Category
-ongelmaamme. Aloitetaan
kuitenkin tutustumalla yleisemmin ratkaisun välineeseen.
Inspiraatiota lift
-metodista
On hyvin yleistä, että jokin asia voi "olla ehkä olemassa". Tällaisia tilanteita löytyy vaikkapa Scalan peruskirjastoista: vektorin alkio indeksillä sata voi joko olla olemassa (riittävän suuressa vektorissa) tai olla olematta (pienemmässä).
Onhan meillä tämä tuttu tyyli tutkia kokoelman alkioita, mutta monesti on ikävää, että se kaatuu liian suureen tai negatiiviseen indeksiin:
val sanoja = Vector("eka", "toka", "kolmas", "neljäs")sanoja: Vector[String] = Vector(eka, toka, kolmas, neljäs) sanoja(2)res0: String = kolmas sanoja(100)java.lang.IndexOutOfBoundsException: 100 [...]
Voisiko olla metodi, joka kertoo meille yhdellä kertaa 1) onko vektorissa alkiota tietyllä indeksillä vai ei, ja 2) jos on, niin mikä?
Voisi olla ja on. Sen nimi on lift
.
sanoja.lift(2)res1: Option[String] = Some(kolmas) sanoja.lift(100)res2: Option[String] = None sanoja.lift(-1)res3: Option[String] = None
Palautusarvot näyttävät aika järkeviltä:
null
; palataan siihen vielä.lift
aaminen ei keskeydy
ajonaikaiseen poikkeustilanteeseen. Metodilla voi poimia
alkion indeksin osoittamasta kohdasta turvallisesti.lift
-metodi nojautuu Option
-nimiseen tietotyyppiin. Selvitetään, mikä se on ja miten
se voi auttaa meitä korjaamaan Category
-luokan.
Option
-tietotyyppi
Option
-luokka on määritelty pakkaukseen scala
ja on siis aina käytettävissä kaikissa
Scala-ohjelmissa. Yksi Option
-olio toimii ikään kuin kääreenä, jonka sisällä on joko
yksi tietyntyyppinen arvo tai vaihtoehtoisesti ei mitään.
Some
ja None
Katsotaan äskeistä esimerkkiä uudestaan:
val sanoja = Vector("eka", "toka", "kolmas", "neljäs")sanoja: Vector[String] = Vector(eka, toka, kolmas, neljäs) sanoja.lift(2)res4: Option[String] = Some(kolmas) sanoja.lift(100)res5: Option[String] = None
Some
-olio on "täysi kääre" eli sellainen Option
-olio, joka
pitää sisällään yhden arvon, tässä tapauksessa merkkijonon.None
on Scala-kieleen kuuluva yksittäisolio, joka edustaa
"tyhjää käärettä". Se on eräänlainen Option
-olio sekin.lift
-metodin palautusarvon tyyppi kertoo meille, että metodi
palauttaa sellaisen Option
-olion, johon on kääritty merkkijono
tai ei mitään. Siis Some
-olion, jossa on merkkijono, tai None
.Otetaan mukaan lukuja sisältävä toinen vektori:
val lukuja = Vector(10, 0, 100, 10, 5, 123)lukuja: Vector[Int] = Vector(10, 0, 100, 10, 5, 123) lukuja.lift(0)res6: Option[Int] = Some(10) lukuja.lift(-1)res7: Option[Int] = None
Huomaa, että tässä tyyppiparametri on toinen. Voit ajatella,
että esimerkiksi Option[Int]
tarkoittaa: "kääre, jonka sisältä löytyy
Int
-arvo tai tyhjää". Hakasulkeisiin merkityn tyyppiparametrin idea
on tässä sama kuin vektorien ja puskurienkin kohdalla: sillä ilmoitetaan,
minkä tyyppisestä "kääreen" sisällä kenties olevasta arvosta on kyse.
Option
-tyyppi on määritelty takamaan meille erinäisiä asioita:
- Kaikki
Some
-oliot ovatOption
-olioita. Vrt. "Kaikki laamat ovat eläimiä."None
on myösOption
-olio. Option
-olioita on täsmälleen kahdenlaisia. JokainenOption
-tyyppinen olio on joko jokinSome
-olio taiNone
. Vrt. "Jokainen ihminen on joko joku kuolevainen tai Chuck Norris."- Jokainen
Option[X]
-tyyppinen arvo on jokoNone
tai sellainenSome
, jonka sisällä on tyyppiä X oleva arvo.
Option
-arvoja muuttujassa
Äskeisissä esimerkissä saimme Option
-tyyppisiä arvoja metodin palauttamina. Voimme myös
itse luoda näitä arvoja sekä määritellä muuttujia, joiden tyyppi on Option
.
var testi: Option[Int] = Nonetesti: Option[Int] = None
None
tai tarkemmin
sanoen viittaus None
-yksittäisolioon.None
on määritelty tyypiltään yhteensopivaksi Option
-luokan
kanssa. Viittauksen None
-olioon voi sijoittaa arvoksi
nimenomaan Option
-tyyppiselle muuttujalle, ei esimerkiksi
Experience
- tai Int
-tyyppiselle.Näin siis määrittelimme muuttujan, jonka avulla voisi tallentaa kokonaisluvun, mutta johon
nyt ei ole kuitenkaan tallennettuna kokonaislukua vaan None
. Option
-tyyppisen muuttujan
arvoksi voi sijoittaa myös "täyden kääreen" eli Some
-olion:
testi = Some(5)testi: Option[Int] = Some(5)
Some(
...)
. Kun
kyseessä on Option[Int]
, täytyy sisällä olevan arvon olla
kokonaisluku. Ilmaisulla Some(5)
siis luodaan "kääre", jonka
sisällä on lukuarvo 5.new Some(5)
, mutta new
-sana on tässä
yhteydessä luvallista jättää pois, ja yleensä jätetäänkin.)Option[Int]
-olioon voi myös kääriä laskutoimituksen lopputuloksen:
testi = Some(10 + 50)testi: Option[Int] = Some(60)
Kukin Option
-olio on tilaltaan muuttumaton. Esimerkiksi kun kerran on luotu olio
ilmaisulla Some(5)
, ei "kääreeseen" laitettua arvoa voi enää vaihtaa toiseksi eikä
käärettä tyhjentää. Sen sijaan jos on olemassa var
-muuttuja, joka viittaa Option
-olioon,
niin muuttujan arvoksi voi kyllä sijoittaa viittauksen johonkin muuhun Option
-olioon,
kuten yllä tehtiinkin.
Kuvia tai sitä ei tapahtunut
Kääritty vs. käärimätön arvo
Int
-tyyppinen arvo on aina kokonaisluku. Option[Int]
-tyyppisessä arvossa on ehkä
sisällä kokonaisluku. Nämä tyypit ovat erilliset eikä niitä voi sijoittaa ristiin.
Esimerkiksi Int
-arvoa ei sellaisenaan voi sijoittaa Option[Int]
-tyyppiseen muuttujaan:
testi = 5<console>:11: error: type mismatch; found : Int(5) required: Option[Int] testi = 5 ^
Option[Int]
-tyyppistä arvoa ei voi vastaavasti sijoittaa Int
-tyyppiseen muuttujaan
tai käyttää laskutoimituksessa:
var lukuarvo = 10lukuarvo: Int = 10 var ehkaArvo: Option[Int] = Some(10)ehkaArvo: Int = Some(10) lukuarvo = ehkaArvo<console>:13: error: type mismatch; found : Option[Int] required: Int lukuarvo = ehkaArvo ^ ehkaArvo - 1<console>:13: error: value - is not a member of Option[Int] ehkaArvo - 1 ^
Samaan tapaan jos metodi ottaa parametrikseen Int
-arvon, ei Option[Int]
kelpaa ja
toisin päin.
Tämä on erittäin hyvä asia. Se tarkoittaa muun muassa sitä, että jos tietyssä kohtaa ohjelmaa kaivataan kokonaislukua, ei riitä, että on "mahdollisesti olemassa oleva luku". Virheilmoitukset muistuttavat jo ennen ohjelma-ajoa, että jos luku tarvitaan, niin sellainen on myös toimitettava.
Option
, kas siinä pulma
Voisiko selventää eroa null
vs. None
, kiitos?
Pythonia osaaville
Python-ohjelmointikielessä on käsite None
, joka vastaa pitkälti
Scalan ja muiden kielten null
ia. Ei siis Scalan None
a.
null
on arvo, joka tarkoittaa "viittaus ei mihinkään olioon" ja jota on teknisesti
mahdollista käyttää varsin vapaasti erilaisissa yhteyksissä, vaikkapa Experience
- tai
String
-tyyppisen muuttujan arvona.
null
-arvojen käyttäminen kuuluu siis ohjelmointitapaan, jossa mikä tahansa arvo on
periaatteessa "valinnainen" ja voi puuttua.
Call me Maybe
?
Juuri ensi kertaa tavattu Option
-luokka esiintyy eri muodoissa
myös muissa ohjelmointikielessä kuin Scalassa, eritoten
funktionaalista ohjelmointia tukevissa
kielissä. Joissakin kielissä (esim. Haskell) sen nimi on Option
in
sijaan Maybe
. Joissakin kielissä (esim. C#) taas on käsite nullable
type, joka on sukua Option
ille.
None
on yksittäisolio, joka on Option
-tyyppinen. Se on siis tarkoitettu käytettäväksi
nimenomaan yhteyksissä, joissa tarvitaan Option
-tyyppistä arvoa. Viittauksen None
en
voi sijoittaa sellaiseen muuttujaan, jonka tyyppinä on Option[
jotain]
mutta ei
mihin tahansa muualle.
None
-arvon ja Option
-tyypin käyttäminen sopii ohjelmointitapaan, jossa ohjelmoija
nimenomaisesti määrittelee ne kohdat ohjelmasta, joissa arvo on "valinnainen" ja voi
puuttua. Jäljempänä luvussa kerrotaan lisää siitä, miksi tämä ohjelmointitapa on hyvin
usein parempi, ja null
in käyttöä on syytä välttää.
Ero null
-viittauksen ja None
-olion välillä voi selkiytyä vertaamalla tämän luvun
animaatioita edellisen luvun lopussa olleeseen.
Ja Unit
vielä?
Luvussa 1.6 ensi kertaa esiintynyt Unit
on erikoisarvo, jota käytetään tarkoittamaan
"ei mitään merkityksellistä arvoa" ja erityisesti: "Funktio ei tuota missään tilanteessa
mitään merkityksellistä palautusarvoa". Unit
-arvo on kokonaan omaa Unit
-tietotyyppiään,
eikä sitä voi sijoittaa mielivaltaiseen muuttujaan (kuten null
in voi) tai
Option
-tyyppiseen muuttujaan (kuten None
n). Tällä kurssilla emme käytä Unit
ia
missään muussa yhteydessä kuin eräiden vaikutuksellisten funktioiden palautusarvona.
Option
-kääreen avaaminen ja match
-käsky
Jotta voimme tehdä Option
-olion sisällöllä jotain, tarvitsemme keinon "avata kääre" ja
katsoa, mitä siellä on, jos mitään.
Otamme nyt työkaluksemme Scalan match
-käskyn. Se on if
-käskyn sukulainen sikäli,
että silläkin voi tehdä valintoja.
Option
in sisältöön pääsee käsiksi muillakin konstein, mutta käytämme nyt aluksi
nimenomaan match
-käskyä. match
-käsky puolestaan sopii moneen muuhunkin kuin
Option
in käsittelyyn, mutta käytämme sitä nyt aluksi nimenomaan siihen.
match
-valintakäsky
Pohjustetaan tutunoloisesti:
val lukuja = Vector(10, 0, 100, 10, 5, 123)lukuja: Vector[Int] = Vector(10, 0, 100, 10, 5, 123)
val ehkaLuku = lukuja.lift(5)ehkaLuku: Option[Int] = Some(123)
val ehkaTuhannes = lukuja.lift(999)ehkaTuhannes: Option[Int] = None
Valitaan toimenpide sen perusteella, kumpaan tapaukseen ehkaLuku
-muuttujan arvo
"mätchää", täyteen vai tyhjään Option
-kääreeseen:
ehkaLuku match { case Some(kaarittyLuku) => "kääreessä on joku luku" case None => "kääreessä ei ole mitään lukua" }res8: String = kääreessä on joku luku
ehkaLuku
arvoa. Sen perään tulevat
avainsana match
ja aaltosulkeet, jotka ovat pakolliset.case
-sanaa sekä yhtäsuuruusmerkistä ja suurempi kuin -merkistä
koostuvaa "nuolta" käyttäen.match
-käskyä
juuri Option
-olioiden käsittelyyn, käytämme tyypillisesti yhtä
Some
-tapausta ja yhtä None
-tapausta.Some
-olio,
niin valituksi tuli ensimmäinen tapaus.match
-käskyllä
muodostetun lausekkeen arvon. Esimerkissämme kyseessä on
merkkijono.kaarittyLuku
on ohjelmoijan valitsema nimi,
jota emme käyttäneet mihinkään ihan vielä.Vastaavasti valituksi voi tulla myös toinen tapaus:
ehkaTuhannes match { case Some(kaarittyLuku) => "kääreessä on joku luku" case None => "kääreessä ei ole mitään lukua" }res9: String = kääreessä ei ole mitään lukua
ehkaTuhannes
-muuttujan arvo sopii yhteen jälkimmäisen
tapauksen kanssa, joten päädytään toiseen haaraan ja saadaan
match
-lausekkeelle arvo sieltä.Äskeisissä esimerkeissä oli väliä vain sillä, oliko kyseisellä Option
-oliolla sisältöä,
ei sillä, mitä tuo mahdollinen sisältö oli. Seuraava esimerkki huomioi myös tämän.
ehkaLuku match { case Some(kaarittyLuku) => "kääreessä on: " + kaarittyLuku case None => "kääreessä ei ole mitään lukua" }res10: String = kääreessä on: 123
Some
-tapauksen määrittelyyn voi kirjata muuttujan nimen tähän
tapaan. Tällöin Option
-olion sisältämä arvo kopioituu uuteen
tuonnimiseen paikalliseen muuttujaan, kun tapauksen koodia
aletaan suorittaa. Niinpä...Option
-olion sisältä "löytyi", tässä esimerkissämme
kokonaislukuun.Seuraava lisäesimerkki korostaa eräitä match
-käskyn ominaisuuksia:
val lukuja = Vector(10, 0, 100, 10, 5, 123)lukuja: Vector[Int] = Vector(10, 0, 100, 10, 5, 123) val tulos = lukuja.lift(4) match { case None => 0 case Some(luku) => luku * 1000 }tulos: Int = 5000
match
-lauseke on lauseke siinä missä muutkin, ja sitä voi
käyttää osana muita käskyjä. Sen arvon voi esimerkiksi sijoittaa
muuttujaan kuten tässä.match
-sanan eteen kirjoitetaan lauseke, jonka arvoa
tarkastellaan. Aiemmissa esimerkeissä tuo lauseke oli muuttujan
nimi, mutta se voi hyvin olla jokin muukin. Tässä kutsutaan
lift
-metodia ja tarkastellaan saman tien sen palauttamaa
arvoa (sijoittamatta sitä välissä muuttujaan).None
-tapauksen voi kirjata ennen Some
-tapausta. Scala käy
tapauksia läpi koodiin kirjatussa järjestyksessä kunnes osuva
löytyy.match
-lausekkeen arvo oli merkkijono,
mutta muutkin tyypit ovat yhtä mahdollisia. Tässä kumpikin
tapaus tuottaa kokonaisluvun, ja tuloksen tyyppi on Int
.case
-alkuiset rivit on tapana sisentää. Tämäkään sisennys ei
kuitenkaan vaikuta ohjelman toimintaan.Voit tutkia esimerkkejä myös animaationa.
Seuraava pseudokoodi kiteyttää, miten olemme soveltaneet match
-käskyä
Option
-olioihin:
tutkittava lauseke match { case Some(muuttujaSisallolle) => lauseke evaluoitavaksi "kääreen ollessa täysi", ehkä oheista muuttujaa hyödyntäen case None => lauseke evaluoitavaksi "kääreen ollessa tyhjä" }
Tapauksiin voi liittää myös vaikutuksellisia käskyjä kuten println
. Tässä esimerkki:
lukuja.lift(7) match { case None => println("Ei ollut lukua seiskaindeksillä.") println("Ei voi mitään.") case Some(luku) => println("Seiskaindeksillä oli " + luku + ".") }Ei ollut lukua seiskaindeksillä. Ei voi mitään.
Pikkutehtäviä: match
plus Option
Ennen kuin korjataan Category
-luokka, tee seuraavat pikkutreenit. Käytä REPLiä apuna
tarpeen mukaan. Option
-tyypistä muodostuu meille tärkeä työväline, joten on hyvä
treenata nämä perusasiat kuntoon.
Suosikkikirjanpito: uusi toteutus
Option
-tyyppinen ilmentymämuuttuja
Palataan Category
-luokkaan. Sinne voi määritellä ilmentymämuuttujan fave
Option
-luokkaa
käyttäen näin:
class Category(val name: String, val unit: String) {
private val experiences = Buffer[Experience]()
private var fave: Option[Experience] = None
def favorite = this.fave
def addExperience(newExperience: Experience) = {
// Toteutus tänne.
}
}
fave
-muuttujan tyypiksi Option[Experience]
eli "mahdollisesti yksi kokemus tai sitten ei yhtään". Tämä
määrittely vastaa täsmälleen mallinnustarvettamme.None
tai Some(
...)
,
missä "kääreen" sisällä on viittaus Experience
-olioon. Aluksi
se on None
.Uusi toteutus addExperience
-metodille
Toteutetaan addExperience
-metodi ensin pseudokoodina:
def addExperience(newExperience: Experience) = { this.experiences += newExperience Suosikin päivittämistapa riippuu siitä, mitä suosikkikääreestä nyt löytyy: 1) Mikäli ei löydy mitään, merkitään suosikiksi Some, jossa on lisätty uusi kokemus. 2) Mikäli kääreessä on vanha suosikki, valitaan uudesta kokemuksesta ja vanhasta suosikista parempi ja kääritään se. }
this.fave
n vanhana arvona on vain tyhjä kääre
None
tai jokin Some
-kääreessä oleva vanha suosikkikokemus.Option[Experience]
-tyyppisiä arvoja ei voi vertailla arvosanan
perusteella, Experience
-tyyppisiä voi. Jotta vertailu onnistuisi,
on vanha suosikki otettava Option
-kääreestä ulos.Tässä sama Scalalla:
def addExperience(newExperience: Experience) = {
this.experiences += newExperience
this.fave match {
case None =>
this.fave = Some(newExperience)
case Some(oldFave) =>
val newFave = newExperience.chooseBetter(oldFave)
this.fave = Some(newFave)
}
}
Some
-kääreeseen
ennen ilmentymämuuttujaan tallentamista (koska uusittuun
fave
-muuttujaan pitää tallentaa Option[Experience]
-tyyppinen
eikä Experience
-tyyppinen arvo).match
-käskyn sisällä voi myös määritellä
apumuuttujia tavalliseen tapaan. Tässä on käytetty
tilapäissäilöä newFave
välivaiheiden selkiyttämiseksi,
mutta koko homman olisi voinut hoitaa myös yhdellä käskyllä
this.fave = Some(newExperience.chooseBetter(oldFave))
Sama toisin jäsennettynä
Voit tutkia myös tätä toista toteutusta, joka toimii yhtä lailla.
def addExperience(newExperience: Experience) = {
this.experiences += newExperience
val newFave = this.fave match {
case Some(oldFave) => newExperience.chooseBetter(oldFave)
case None => newExperience
}
this.fave = Some(newFave)
}
Muutos luokan rajapinnassa
Äsken tehtyä Category
-toteutusta (joka myös GoodStuff-moduulin tiedostoista löytyy)
käytetään hieman eri tavalla kuin edellisessä luvussa hahmottelimme.
Nythän nimittäin favorite
-metodin palautusarvon tyyppi on Experience
n sijaan
Option[Experience]
, ja metodi palauttaa joko None
n tai kokemusolioon viittavan Some
n.
Luokkaa käyttäessä on huomioitava tämä muutos. Uutta Category
-luokkaamme voi koekäyttää
vaikkapa näin:
val wineCategory = new Category("Wine", "bottle")
// ... (Mahdollisesti lisätään kokemusolioita kategoriaan.)
wineCategory.favorite match {
case Some(bestWine) =>
println("Suosikki on: " + bestWine.name)
case None =>
println("Ei suosikkia vielä.")
}
Toisaalta esimerkiksi lauseke wineCategory.favorite.name
ei enää ole kelvollinen kuten
ei sovi ollakaan. Jos yritämme kirjoittaa tuon käskyn ohjelmaamme, saamme virheilmoituksen
jo ennen ohjelma-ajoa: työkalustomme toteaa, että wineCategory.favorite
tuottaa
Option
in, jolla ei ole name
-muuttujaa (vaan sen ehkä sisältämällä Experience
-oliolla
on).
Vaihtoehtoinen toteutustapa ilman Option
ia
Kuten luvun alussa tuli todettua, on toinenkin tapa lähestyä poikkeustilanteen aiheuttanutta
null
-ongelmaa: jätetään null
-alkuarvo suosikille, mutta käytetään käskyjä huolellisesti
niin, ettei olemattoman olion piirteisiin koskaan yritetä päästä käsiksi. Esimerkiksi
addExperience
-metodin olisi voinut saada toimimaan myös ilman Option
-luokkaa:
class Category(val name: String, val unit: String) {
private val experiences = Buffer[Experience]()
private var fave: Experience = null
def favorite = this.fave
def addExperience(newExperience: Experience) = {
this.experiences += newExperience
this.fave =
if (this.fave == null)
newExperience
else
newExperience.chooseBetter(this.fave)
}
}
Kun ensin tarkastetaan vertailuoperaattorilla, onko olion fave
-muuttujan arvo null
,
ja sijoitetaan paremmuusvertailu else
-olioon, ei metodimme tee sopimatonta
paremmuusvertailua olemattoman arvosanan kanssa. Tämäkin metoditoteutus toimii
moitteettomasti.
Et sitten aikaisemmin kertonut!?
Eikö tuo null
-arvoa käyttävä versio ole yhtä toimiva ja yksinkertaisempi kuin
Option
-versio? Monimutkaistiko Option
Category
-luokkaamme aivan turhaan?
Kannattiko tätä uutta tekniikkaa edes opetella?
Option
-toteutuksessa on erittäin hyviä puolia null
-toteutukseen verrattuna.
Käsittelemme niistä eräitä seuraavaksi.
Miljardin dollarin virhe
Kutsun sitä miljardin dollarin virheekseni, null-viittauksen keksimistä vuonna 1965. Suunnittelin silloin ensimmäistä kattavaa tyyppijärjestelmää viittauksille olio-ohjelmointikielessä (ALGOL W). Tavoitteeni oli varmistaa, että kaikki viittausten käyttö olisi ehdottoman turvallista. — — Mutta en pystynyt vastustamaan kiusausta laittaa null-viittaus mukaan pelkästään siksi, että se oli niin helppo toteuttaa. Tämä on johtanut lukemattomiin virheisiin, haavoittuvuuksiin ja järjestelmien kaatumisiin, jotka ovat luultavasti aiheuttaneet miljardin dollarin verran tuskaa ja vahinkoa neljänkymmenen viime vuoden aikana.
—C. A. R. Hoare (käännetty englannista)
Joku kevytpää siis sotkenut asiat?
Ei. Kyseinen herra on poikkeuksellisen merkittävä ja tunnettu tiedemies, joka on muun muassa voittanut Turing-palkinnon eli "tietojenkäsittelyn Nobelin".
Yllä on lainaus puheesta vuodelta 2009, jossa Sir Charles Antony Richard Hoare pyytää
anteeksi null
-arvon keksimistä alun perin ALGOL-ohjelmointikieleen. ALGOLista on otettu
valtavasti (enimmäkseen hyviä) vaikutteita myöhempiin ohjelmointikieliin, nykyisiinkin.
Myös null
-arvo tai jokin sitä vastaava toisenniminen käsite on ohjelmointikielissä
yleinen. Kuitenkin null
-viittausten käyttö aiheuttaa joka päivä jotakuinkin vastaavia
ajonaikaisia virhetilanteita kuin se, jonka näit edellisen luvun lopussa. Usein nämä
virheet ovat monimutkaisemmissa ohjelmissa ja hankalammin paikannettavia kuin esimerkissämme.
Eräät ohjelmoijien piirissä tunnetuimmat ja pahamaineisimmat virheilmoitukset (esim.
segmentation fault ja edellisen luvun null pointer exception) liittyvät ohjelmoijan
tekemiin null
-arvojen käyttövirheisiin.
Miksi Option
?
Kuten olet jo nähnyt, Scala-kielikin mahdollistaa null
-arvojen käytön. Kieli on kuitenkin
laadittu kannustamaan vaihtoehtoisten ratkaisutapojen käyttöön, joista Option
on yksi.
null
-arvon käyttö on (hyvissä) Scala-ohjelmissa harvinaista.
Kun lausekkeen tyyppinä on Option[X]
eikä vain X
, niin emme voi yksinkertaisesti
käyttää sitä kohdassa, jossa kaivataan X
-tyyppistä arvoa. Yritys tehdä näin aiheuttaa
virheilmoituksen jo ennen ohjelman ajamista. Niinpä ohjelmoijan on huolehdittava
"kääreen avaamisesta". Tämä muistuttaa häntä käsittelemään myös None
-tapauksen,
jossa "kääreessä" ei ole arvoa lainkaan.
Esimerkiksi kun Category
n favorite
-metodi palauttaa Option[Experience]
-tyyppisen
arvon, on tämä selvä signaali luokkaa käyttävälle ohjelmoijalle: suosikkia ei välttämättä
ole, ja molemmat tapaukset on syytä jotenkin huomioida metodia käyttäessä. Jos ohjelmoija
asian silti jättää huomioimatta, hän saa pikaisen virheilmoituksen eikä ongelma jää
bugiksi ohjelmaan ja ilmene kenties vasta epäonnisen loppukäyttäjän kärsiessä siitä
joskus myöhemmin.
Voi olla, että saamme luokan kuten Category
toteutettua null
-arvoja käyttäen siten,
että tuon luokan metodit huolellisesti valvovat ettei virhetilanteita synny. Silti
ongelma ei poistu, jos luokan rajapintaan kuuluvat metodit (kuten favorite
) palauttavat
null
-arvoja. Luokan käyttäjän, joka voimme olla me itse tai joku muu, pitää muistaa
olla varpaillaan niiden varalta.
Jos olet ohjelmoinnin aloittelija tai paatunut null
-arvoilla ohjelmoija, et ehkä
aavistakaan, kuinka monelta ongelmalta vältyt käyttämällä "ehkä olemattomien arvojen"
kuvaamiseen Option
ia.
Virheilmoituksista
Option
-ratkaisumallin hyväksi puoleksi mainittiin se, että saa virheilmoituksia, mikä
saattaa kuulostaa oudolta.
Ohjelmoinnin aloittelija voi pitää virheilmoituksia puhtaasti ikävinä. Ennen ohjelma-ajoa tuleva virheilmoitus on kuitenkin ystäväsi! Se tarkoittaa, että tietokone on kyennyt automaattisesti havaitsemaan ohjelmassa jonkin ongelman. On hienoa, että saat viestin — vaikka sitten valituksenkin — välittömänä palautteena. Ajonaikaisten virheiden syiden etsiminen on usein paljon hankalampaa, ja jo niiden havaitseminen on kaikkea muuta kuin itsestään selvää.
Hyvä osa siitä Tony Hoaren miljardista olisi säästetty aikaisemmilla virheilmoituksilla.
Lisämateriaalia Option
in ympäriltä
Alla olevat laatikot täydentävät äskeistä tekstiä. Lue, jos ei ole liian kiire.
Onko Option
vain Scalan ja sen lähisukulaisten erikoisuus?
Ei. Sama tai samantapainen rakenne on käytettävissä monessa muussakin kielessä. On yleistymään päin.
Useissa yleisissä kielissä (esim. C, Java) kyllä käytetään usein
null
-arvoja, joskus syystä (ks. seuraava lisätietolaatikko alla)
ja joskus syyttä. Tuoreimmissa Java-kielen versioissakin on
eräänlainen Optional
-luokka. Nähtäväksi jää, millaisen
käyttäjäkunnan se saavuttaa.
Tämä ei ole puhtaasti kielikysymys. Jos Option
-luokkaa tai
vastaavaa työkalua ei ole, mutta sellaista haluaa käyttää, niin
sellaisen voi tehdä itsekin. Scalassakaan Option
ei varsinaisesti
ole muuta kuin peruskirjastoon kuuluva luokka. Alla on Option
in,
Some
n ja None
n lähdekoodi karsitussa muodossa niiltä osin,
joita kurssilla on tähän mennessä käsitelty.
Seuraavaa koodia ei ole pakko kokonaan ymmärtää, mutta se toimii näyttönä siitä, että jos tämä tyyppi Scalasta puuttuisi, niin sen toteuttaminen ei olisi mikään taikatemppu. Itse asiassa iso osa tästä koodista on jo tämän kurssin alkupään perusteella ymmärrettävissä.
abstract class Option[InnerType] {
def getOrElse(default: =>InnerType) = if (this.isEmpty) default else this.get
def get: InnerType // toteutettu erikseen alakäsitteille Some ja None tuossa alempana
def isEmpty: Boolean // toteutettu erikseen alakäsitteille Some ja None tuossa alempana
def isDefined = !this.isEmpty
}
class Some[InnerType](val content: InnerType) extends Option[InnerType] {
def isEmpty = false
def get = this.content
}
object None extends Option[Nothing] {
def isEmpty = true
def get = throw new NoSuchElementException
}
// Yksinkertaistettu Scala API:n Option-toteutuksesta, joka on täällä:
// https://github.com/scala/scala/blob/v2.11.8/src/library/scala/Option.scala
Yleisempi opetus tästä on, että ohjelmoija voi toteuttaa itselleen ja muille työkaluja, jotka muovaavat tapaa, jolla ohjelmoidaan.
Toki se, että tietty rakenne — vaikkapa Option
— on osa kielen valmista
työkalupakkia ja sitä käytetään kielen peruskirjastoissa, vaikuttaa tuon rakenteen
suosioon kieltä käyttävien ohjelmoijien keskuudessa. Osittain asia on siis kyllä
kielikysymyskin.
Miksi Scalassa sitten edes on null
? Miksi se esiteltiin kurssilla?
Osa ohjelmoijista onkin sitä mieltä, että esimerkiksi Scalassa ei null
-arvoa pitäisi
ollakaan.
Tilanteesta riippuen null
-arvon käyttäminen saattaa olla perusteltua. Scalassa
se on mukana lähinnä parantamassa Scalan yhdisteltävyyttä muiden kielten kanssa.
null
in sisällyttäminen kieleen tekee helpommaksi eritoten sen, että Scala-ohjelmasta
voi käyttää Java-kielellä kirjoitettuja kirjastoja (mistä vähän lisää luvussa 5.4
ja myöhemmin).
We want to move away from null. Null is a source of many, many errors. We could have come out with a language that just disallows null as a possible value of any type, because Scala has a perfectly good replacement called an option type. But of course a lot of Java libraries return nulls and we have to treat it in some way.
Tulevissa Scala-versioissa kuulemma pyritään
kitkemään null
-arvoja uusilla tavoilla.
Muissa kielissä null
ia saatetaan käyttää:
- kätevien vaihtoehtojen puutteen vuoksi;
- tapauskohtaisista suoritustehokkuus- tai kätevyyssyistä;
- ohjelmointikulttuurillisista syistä; tai joskus vain
- koska ei muutakaan osata.
Option
vaatii lisätoimenpiteitä tietokoneelta ohjelman suorituksen aikana
(Some
-olioiden luominen, arvojen noutaminen niiden sisältä). Usein tällä ei ole
mitään käytännön merkitystä, mutta joskus on.
Kuten todettu, emme jatkossa suosi null
iin perustuvia ratkaisuja. Silti
Scala-kieliselläkin ohjelmoinnin alkeiskurssilla null
in esittely on perusteltua, koska:
null
-arvo on monessa muussa yhteydessä yleinen ja kuuluu ohjelmoijan yleissivistykseen. Kun etsit itse tietoa netistä tai tutustut muiden tekemään koodiin, törmäät varmastinull
-arvoon ennemmin tai myöhemmin.- Tämä ei ole vain Scala-kurssi tai edes ensisijaisesti Scala-kurssi.
- Aiheen käsitteleminen mahdollisti eri ratkaisutapojen vertailun sekä lyhyen pohdinnan ohjelmointikielten suunnitteluperiaatteista, joka auttaa ymmärtämään, miksi teemme niin kuin teemme.
- Vaikka välttäisitkin
null
in käyttöä, voit päätyä vastatustenNullPointerException
in kanssa, ja on syytä tuntea vihollisensa.
Lisää jorinaa Option
ista
On toki totta, että Option
in hyödyllisyys riippuu tilanteesta, tarkoituksesta
ja osin ohjelmoijastakin. On toki myös totta, että tilaa virheille jää, vaikka
välttäisikin null
in käyttöä. Aina jää.
Option
illa on puolensa. Sen hyviä puolia on helpompi aliarvoida kuin huonoja.
Olit itse mitä mieltä tahansa, niin Option
ia käytetään laajasti mm.
Scala-ohjelmoinnissa ja siksi tämä tekniikka on syytä hallita.
Tällä kurssilla käytämme Option
-luokkaa jatkossa runsaasti.
Yhteenvetoa
- "Ehkä olemassa olevasta arvosta" voi pitää kirjaa
Option
-luokan avulla. Sille määrätään käyttöyhteyteen sopiva tyyppiparametri: esimerkiksiOption[Int]
tarkoittaa "nolla tai yksi kokonaislukuarvoa".Option
-tyyppinen arvo on joko viittausNone
-olioon ("tyhjään kääreeseen") tai sellaiseenSome
-olioon, jonka sisälle on "kääritty" yksi arvo.- On lähes aina parempi käyttää
Option
ia kuinnull
-viittauksia.
- Scalan
match
-käsky sopii mm.Option
-olioiden käsittelyyn. Sillä voi valita toimenpiteen tapauskohtaisesti sillä perusteella, onkoOption
-oliolla sisältöä vai ei. - Monesti hyödyllisiä
Option
-luokan metodeita ovat mm.getOrElse
,isDefined
jaisEmpty
. LisääOption
-olioiden metodeita kohdataan myöhemmin kurssilla (luku 8.2), jolloin luokan käyttö kätevöityy. - Ohjelmointikieli peruskirjastoineen voidaan suunnitella
vähentämään ohjelmavirheitä ja edesauttamaan virheiden nopeaa
havaitsemista.
Option
-luokka on esimerkki tästä. - Lukuun liittyviä termejä sanastosivulla:
Option
,null
-viittaus; käännösaikainen virhe, ajonaikainen virhe.
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 ja Nikolas Drosdek yhteistyössä Juha Sorvan, Otto Seppälän, Arto Hellaksen ja muiden kanssa.
Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.
Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.
kolmas
".