Luku 4.3: Olemattomia arvoja
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 voi 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ä, ettemme yritä 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 is out of bounds (min 0, max 3) [...]
Voisiko olla metodi, joka kertoo meille yhdellä kertaa 1) onko vektorissa alkiota tietyllä indeksillä vai ei, ja 2) jos on alkio, 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
Paluuarvot näyttävät aika järkeviltä:
Kun alkiota ei löydy, paluuarvo on "ei arvoa lainkaan".
Huom. Tämä ei ole sama kuin null
; palataan siihen vielä.
Vaikka indeksi olisi epäkelpo, lift
aaminen ei keskeydy
ajonaikaiseen poikkeustilanteeseen. Metodilla voi poimia
alkion indeksin osoittamasta kohdasta turvallisesti.
Paluuarvot eivät kuitenkaan ole tavallisia merkkijonoja vaan jotakin muuta.
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 paluuarvon 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
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 takaamaan 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
.Jokainen
Option[X]
-tyyppinen arvo on jokoNone
tai sellainenSome
, jonka sisällä on X-tyyppinen 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
Tässä muuttujan arvoksi on sijoitettu aluksi 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)
Käärityn arvon saa luotua ilmaisulla 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.
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" pantua 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-- Type Mismatch Error: |testi = 5 | ^ | Found: (5 : Int) | Required: Option[Int]
Vastaavasti Option[Int]
-tyyppistä arvoa ei voi 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-- Type Mismatch Error: |lukuarvo = ehkaArvo | ^^^^^^^^ | Found: (ehkaArvo : Option[Int]) | Required: Int ehkaArvo - 1-- Type Mismatch Error: |ehkaArvo - 1 |^^^^^^^^^^ |value - is not a member of Option[Int]
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
Tyyppipäättely, muuttujat ja Option
Opiskelija kysyy:
En ymmärrä tätä, kun sanotaan, että: "Kaikki
Some
-oliot ovat Option
-olioita" ja
"Jokainen Option
-tyyppinen olio on joko
jokin Some
-olio tai None
". Miten sitten
kuitenkin voi tehdä näin:
val eka = Some(10)
Tuleeko tuossa eka
-muuttujasta Option
-olio?
On tärkeää erottaa toisistaan muuttujat ja oliot. Mikään muuttuja ei varsinaisesti ole mikään olio eikä sellaiseksi tule.
Jos muuttujan tyyppi on Option[X]
, se voi viitata joko Some[X]
-tyyppiseen olioon tai None
-yksittäisolioon.
Muotoillaan kysymys toisin: mikä tulee sellaisen muuttujan tyypiksi,
joka pannaan noin viittaamaan Some
-olioon?
var kokeilu1 = Some(10)kokeilu1: Some[Int] = Some(10)
Muuttujan arvoksi asetetaan Some(10)
. Scala
päättelee muuttujan tyypiksi Some[Int]
.
Tähän muuttujaan ei voi sijoittaa mitään
muuta kuin viittauksia Some[Int]
-olioihin.
Ei esimerkiksi None
a. (Tällaisen muuttujan
tekeminen ei yleensä ole perusteltua. Jos
tiedetään, että kokonaislukuarvo on olemassa,
pelkkä Int
riittää.)
Kirjaamalla muuttujan määrittelyyn laajempi tyyppi
Option[Int]
saadaan muuttuja, joka voi viitata
myös None
-olioon, kuten alla.
var kokeilu2: Option[Int] = Some(10)kokeilu2: Option[Int] = Some(10) kokeilu2 = Nonekokeilu2: Option[Int] = None
Tyyppipäättelyn kannalta ratkaisevaa on se, mitä muuttujaan
sijoitetaan, kun muuttuja määritellään. Scala ei lähde "yleistämään"
muuttujan tyyppiä (esim. Some
sta yläkäsite Option
iksi), ellei
ohjelmoija siihen erikseen kehota kuten kokeilu2
-muuttujan
kohdalla yllä. Tai ellei yhtäsuuruusmerkkiä seuraava lauseke
vaadi yleistämistä, kuten tässä:
val luku = 100luku: Int = 100 var mahdollinenPositiivinenLuku = if luku > 0 then Some(luku) else NonemahdollinenPositiivinenLuku: Option[Int] = Some(100)
Vaihtoehtoisten lausekkeiden arvot ovat tyyppiä Some[Int]
ja None
.
Koko if
-lausekkeen tyyppi on sellainen, että se kattaa
molemmat mainitut tapaukset. Siis Option[Int]
.
Sijoitettavan lausekkeen tyyppi tulee myös muuttujan tyypiksi. Muuttujan tyyppi "lukitaan" sijoituksen yhteydessä, eikä se voi vaihtua.
Voisiko selventää eroa null
vs. None
, kiitos?
null
on arvo, joka tarkoittaa "viittaus ei mihinkään olioon" ja jota on teknisesti
mahdollista käyttää hyvinkin vapaasti erilaisissa yhteyksissä. Sitä on mahdollista
käyttää vaikkapa Experience
- tai String
-tyyppisen muuttujan arvona.
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 Option
-tyyppiseen muuttujaan muttei mihin tahansa muualle.
null
-arvojen käyttäminen kuuluu ohjelmointitapaan, jossa mikä tahansa arvo
on "valinnainen" ja saattaa puuttua. Tästä poiketen None
-arvon ja Option
-tyypin
käyttäminen sopii ohjelmointitapaan, jossa ohjelmoija nimenomaisesti määrittelee
ne kohdat ohjelmasta, joissa arvo on "valinnainen" ja saattaa puuttua. Jäljempänä
tässä luvussa kerrotaan lisää siitä, miksi jälkimmäinen ohjelmointitapa on usein
parempi ja null
ia on syytä välttää — erityisesti jos ohjelmasi ratkaisevat
mutkikkaita ja tärkeitä ongelmia ja ovat kooltaan suuria.
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 esiintyi ensi kertaa erikoisarvo Unit
. Sitä käytetään tarkoittamaan
"ei mitään merkityksellistä arvoa" ja erityisesti "funktio ei missään tilanteessa
tuota mitään merkityksellistä paluuarvoa".
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 voi). Käytämme Unit
ia sellaisten vaikutuksellisten funktioiden
paluuarvona, jotka "eivät palauta mitään".
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
Tutkimme tässä lausekkeen ehkaLuku
arvoa. Sen perään tulevat
avainsana match
ja rivinvaihto.
Käsiteltävänä on kaksi erilaista tapausta. Ne kirjataan case
-sanaa ja "nuolta" käyttäen. "Nuoli" koostuu yhtäsuuruusmerkistä
ja suurempi kuin -merkistä.
Siinä välissä määritellään tapaukset. Kun käytämme match
-käskyä
juuri Option
-olioiden käsittelyyn, käytämme tyypillisesti yhtä
Some
-tapausta ja yhtä None
-tapausta.
Koska esimerkkilausekkeemme arvo sattui olemaan eräs Some
-olio,
niin valituksi tuli ensimmäinen tapaus.
Nuolen jälkeinen lauseke määrää koko match
-käskyllä muodostetun
lausekkeen arvon. Esimerkissämme kyseessä on merkkijono.
Esimerkkikäskyssämme 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ä...
... muuttujan nimeä voi käyttää viittaamaan siihen arvoon,
joka 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 * 1000tulos: 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 tuota arvoa välissä muuttujaan).
Tapauksilla ei ole mitään ennalta määrättyä järjestystä;
None
-tapauksen voi kirjata ennen Some
-tapausta. Scala käy
tapauksia läpi koodiin kirjatussa järjestyksessä kunnes osuva
löytyy.
Aiemmissa esimerkeissä match
-lausekkeen arvo oli merkkijono,
mutta muutkin tyypit ovat yhtä mahdollisia. Tässä kumpikin
tapaus tuottaa kokonaisluvun, ja tuloksen tyyppi on Int
.
Sisennä case
-alkuiset rivit.
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.
Kun käskyt ovat vaikutuksellisia, vaihda riviä ja sisennä syvemmälle.
Käskyjä voi olla peräkkäin useitakin.
Pikkutehtäviä: match
ja Option
in metodeita
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
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.
end Category
Asetetaan fave
-muuttujan tyypiksi Option[Experience]
eli "mahdollisesti yksi kokemus tai sitten ei yhtään".
Tämä määrittely vastaa täsmälleen mallinnustarvettamme.
Muuttujan arvo voi siis olla joko 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.
Kaksi tapausta: this.fave
n vanhana arvona on vain tyhjä kääre
None
tai jokin Some
-kääritty 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)
Lisäyksen lopuksi kategoriaolioon liittyy väistämättä
jokin suosikkikokemus. Se tallennetaan Some
-kääreeseen
ennen ilmentymämuuttujaan tallentamista (koska uusittuun
fave
-muuttujaan pitää tallentaa Option[Experience]
-tyyppinen eikä Experience
-tyyppinen arvo).
Kuten näet, 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 ja on kenties nätimpi. (Makuasia.)
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 paluuarvon 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 = 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 then
newExperience
else
newExperience.chooseBetter(this.fave)
end Category
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 seuraavaksi niistä eräitä.
Miljardin dollarin virhe
Kutsun null-viittauksen keksimistä vuonna 1965 miljardin dollarin virheekseni. 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 ottaa 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)
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 sitä vastaava toisen niminen 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 vaihtoehtoisiin ratkaisuihin, joista Option
on yksi.
null
-arvon käyttö on (hyvissä) Scala-ohjelmissa harvinaista.
Kun lausekkeen tyyppinä on Option[X]
eikä vain X
, 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 pitäisi 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. Se
saattaa kuulostaa oudolta.
Ohjelmoinnin aloittelija voi pitää virheilmoituksia puhtaasti ikävinä. Mutta ennen ohjelma-ajoa tuleva virheilmoitus on 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ä
Seuraavat 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 then 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 APIn 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ä muihin kieliin. null
in
sisällyttäminen kieleen tekee helpommaksi sen, että Scala-ohjelmasta voi käyttää
Java-kielellä kirjoitettuja kirjastoja. (Siitä 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.
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 hieman 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.Scala on kiva kieli, mutta tämä ei ole vain Scala-kurssi tai edes ensisijaisesti Scala-kurssi. Tämä on ensisijaisesti ohjelmoinnin perusteiden kurssi.
Kun käsittelemme tämän aiheen, voimme vertailla eri ratkaisutapoja ja pohtia lyhyesti ohjelmointikielten suunnitteluperiaatteista. Tämä auttaa ymmärtämään, miksi ohjelmoimme niin kuin ohjelmoimme.
Vaikka välttäisitkin
null
-arvon käyttöä, voit päätyä vastatustenNullPointerException
in kanssa. Tunne vihollisesi.
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äisitkin 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 joka tapauksessa
käytetään laajasti Scala-ohjelmoinnissa ja muillakin kielillä
ohjelmoidessa. Tämä tekniikka on siis syytä hallita.
Tällä kurssilla käytämme Option
-luokkaa jatkossa runsaasti.
Yhteenvetoa
"Ehkä olemassa olevasta arvosta" voi pitää kirjaa
Option
-luokan avulla. Luokalle 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.3), 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, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Joel Toppinen, 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, Juha Sorva ja Jaakko Nakaza. 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; sitä 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.
Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.
Kun alkio löytyy, paluuarvo on "joku merkkijono, tarkemmin sanoen
kolmas
".