Luku 7.4: Suljettuja tyyppejä ja lueteltuja olioita
Suljetut tyypit
Johdanto
Jos määrittelemme piirreluokan kuten luvussa 7.3 teimme, voi tuolle piirreluokalle määritellä alatyyppejä vapaasti: samassa tiedostossa, toisessa tiedostossa, ihan toisessa pakkauksessa, missä vain. Jos ohjelmiston parissa työskentelee usea ohjelmoija, kuka vain heistä voi määritellä piirreluokalle mitä vain alatyyppejä. Pahimmassa tapauksessa tällä on arvaamattomiakin seurauksia piirreluokkaamme käyttävälle koodille.
Melko usein pystymme kuitenkin jo etukäteen päättämään, mitkä kaikki alatyypit tietylle piirreluokalle haluamme. Tällöin on hyvä ajatus ilmoittaa koodissakin, että "tällä piirreluokalla on nämä alatyypit ja siinä kaikki — muita ei saa eikä voi tehdä". Tässä luvussa tutkimme tätä ajatusta.
Esimerkki: varusmies- ja siviilipalvelus
Mallinnetaan ohjelmassa, onko henkilö suorittanut suomalaisen varusmies- tai siviilipalveluksen sekä hieman lisätietoa mahdollisesti suoritetusta palveluksesta. Kuvataan tätä tietoa yleisesti piirreluokalla, joka on aivan yksinkertainen:
trait Palvelus
Laaditaan sille alatyyppejä:
Varusmiespalveluksen voi suorittaa aseellisena tai aseettomana, mitä kuvaa piirreluokka
Varusmiespalvelus
alikäsitteineen:
trait Varusmiespalvelus(val haara: String) extends Palvelus
class Aseellinen(haara: String) extends Varusmiespalvelus(haara)
class Aseeton(haara: String) extends Varusmiespalvelus(haara)
Nyt esimerkiksi merivoimissa suoritettua aseellista palvelusta kuvaamaan voi luoda olion
Aseellinen("merivoimat")
.
Siviilipalvelusta kuvaa oma luokkansa. Siviilipalvelus suoritetaan jonkin tietyn tahon
(esim. yliopiston) palveluksessa, minkä kirjaamme kuhunkin Siviilipalvelus
-olioon:
class Siviilipalvelus(val paikka: String) extends Palvelus
Tyyppihierarkiamme täydentää haara, joka kuvaa sitä, että henkilö ei ole suorittanut varusmies- tai siviilipalvelusta:
trait Suorittamatta extends Palvelus
class Vapautettu(val perustelu: String) extends Suorittamatta
object Valitsematta extends Suorittamatta
Yksittäisoliolla Valitsematta
kuvataan tarkemmin
erittelemättä kaikki sellaiset tapaukset, joissa
henkilöä ei ole (ainakaan vielä) sijoitettu mihinkään
varusmies- tai siviilipalveluksen muotoon eikä
vapautettu. Palvelus on siis suorittamatta.
Ohjelmassamme on tarkoitus, että mikä tahansa Palvelus
-tyyppinen olio on aina
jotakin tyypeistä Varusmiespalvelus
, Siviilipalvelus
tai Suorittamatta
. Haluamme,
että on mahdotonta, että joku tulisi tehneeksi jonnekin päin ohjelmaa uuden alatyypin
Palvelus
-tyypille. Haluamme, että Palvelus
ta käyttävä ohjelmoija voi luottaa
siihen, että esimerkiksi seuraava koodi kattaa kaikki tapaukset:
def kokeileKuvauksia() =
val esimerkkeja =
Vector(Aseellinen("merivoimat"), Valitsematta, Siviilipalvelus("Aalto"),
Vapautettu("terveys"), Aseeton("maavoimat"))
esimerkkeja.map(kuvaaPalvelus).foreach(println)
def kuvaaPalvelus(palvelus: Palvelus): String =
palvelus match
case varusmies: Varusmiespalvelus => s"varusmiespalvelus (${varusmies.haara})"
case siviili: Siviilipalvelus => s"siviilipalvelus (${siviili.paikka})"
case eiPalvellut: Suorittamatta => "palvelus suorittamatta"
Meillä on erilaisia Palvelus
-olioita vektorissa. Teemme
kustakin kuvauksen kutsumalla funktiotamme kuvaaPalvelus
.
match
-käskymme valitsee dynaamisen tyypin mukaan (luku 7.3),
mikä haara valitaan ja siis millainen merkkijono muodostetaan.
Haluamme lyödä lukkoon, että nämä kolme ovat Palvelus
-tyypin
välittömät alatyypit. Haluamme näin estää kuvaaPalvelus
-funktiotamme (tai vastaavaa Palvelus
ta käyttävä koodia)
kaatumasta ajon aikana. Noin nimittäin käy, jos joku määrittelee
uuden Palvelus
-alatyypin ja välittää funktiolle olion, jota se
ei ole varautunut käsittelemään.
Saamme halutun takeen, kun teemme Palvelus
-tyypistä suljetun.
Piirreluokan sulkeminen: sealed
Yksi lisäsana riittää:
sealed trait Palvelus
// Jne. Muut alatyypit samassa tiedostossa.
Sana sealed
sulkee piirreluokamme. Palvelus
-tyypille on siis
mahdotonta määritellä välittömiä alatyyppejä muualla kuin tässä
kyseisessä tiedostossa. Kääntäjä valvoo tätä tiukasti.
Palvelus
-tyypin käyttäjä voi luottaa siihen, että sen alla tyyppihierarkiassa on vain
nuo kolme haaraa.
Suljetaanpa saman tien myös Suorittamatta
ja Varusmiespalvelus
-piirreluokat. Tässä koko
tyyppihierarkiamme kootusti:
sealed trait Palvelus
sealed trait Varusmiespalvelus(val haara: String) extends Palvelus
class Aseellinen(haara: String) extends Varusmiespalvelus(haara)
class Aseeton(haara: String) extends Varusmiespalvelus(haara)
class Siviilipalvelus(val paikka: String) extends Palvelus
sealed trait Suorittamatta extends Palvelus
class Vapautettu(val perustelu: String) extends Suorittamatta
object Valitsematta extends Suorittamatta
Näin on taattu, että mikä tahansa Palvelus
-olio on aina joko tyyppiä Aseellinen
,
Aseeton
, Siviilipalvelus
tai Vapautettu
tai sitten kyseessä on yksittäisolio
Valitsematta
.
Vertailukohdaksi voi ottaa vaikkapa tutun Option
-tyypin: sen käyttäjä voi aina luottaa
siihen, että Option
-tyyppinen olio on joko jokin Some
-olio tai yksittäisolio None
.
Muita mahdollisuuksia ei voi olla, eikä sellaisiin tarvitse varautua. (Scala APIn Option
onkin suljettu juuri sealed
-sanalla; luokka Some
ja yksittäisolio None
on määritelty
sen kanssa samassa kooditiedostossa.)
Suljettu tyyppi ja match
Konkreettinen lisähyöty piirreluokan sulkemisesta on, että kääntäjä varoittaa mahdollisista ongelmista paremmin. Esimerkki:
def kuvaaPalvelusHuonosti(palvelus: Palvelus): String =
palvelus match
case varusmies: Varusmiespalvelus => s"varusmiespalvelus (${varusmies.haara})"
case siviili: Siviilipalvelus => s"siviilipalvelus (${siviili.paikka})"
case vapautus: Vapautettu => s"vapautettu palveluksesta (syy: ${vapautus.perustelu})"
Funktiomme käsittelee kolme eri tapausta.
Scala-kääntäjä varoittaa match may not be exhaustive,
eli kaikkia tapauksia ei ole huomioitu. On siis
jäänyt määrittelemättä, mitä tietyssä tilanteessa
pitäisi tapahtua. Varoitus on aiheellinen, sillä
tämä funktio kaatuu, jos sille antaa parametriksi
Valitsematta
-olion.
Ilman sealed
-määrettä kääntäjä ei moista varoitusta anna.
Jos piirreluokkaa ei ole suljettu, ei ole lainkaan takeita, että
tällainen match
-käsky kattaisi kaikki tapaukset tyhjentävästi.
Valinnaista jatkoa esimerkille
Tavallisen luokan sulkemisesta
Edellä suljimme piirreluokkia. Myös tavallisen luokan voi sulkea
sealed
-sanalla. Esimerkiksi Siviilipalvelus
-luokan voisi
määritellä näin:
sealed class Siviilipalvelus(val paikka: String) extends Palvelus
Nyt tälle luokalle ei voi määritellä alatyyppejä kuin samassa tiedostossa. Opetetun valossa ajatus saattaa kuulostaa oudolta, koska olemme tähän mennessä määritelleet alatyyppejä piirreluokille eikä tavallisille luokille. Kuitenkin sopivissa tilanteissa myös tavalliselle luokalle voi määritellä alatyyppejä, kuten teemme seuraavassa luvussa 7.5.
Tavallisten luokkien kohdalla käytettävissä on myös vielä jyrkempi
määre, final
, joka ilmoittaa, että luokalla ei ole alatyyppejä
lainkaan — ei tässä eikä missään muuallakaan määriteltyjä.
Siitäkin kerrotaan hieman lisää seuraavassa luvussa.
Tapausluokat ja match
-käsky
Luvun 4.4 valinnaisessa materiaalissa esiteltiin tapausluokan (case class) käsite. Käytimme siellä tällaista luokkaa:
case class Album(val name: String, val artist: String, val year: Int)
Totesimme tällaiset tapausluokat käteviksi match
-käskyn yhteydessä.
match
-käskyn case
-haarat voi määritellä käyttäen tapausluokan
nimeä ja luontiparametreja vastaavia osasia. Tuollainen haara "purkaa"
olion ja poimii mainitut tiedot, kuten tässä:
jokinAlbumiolio match
case Album(_, _, vuosi) if vuosi < 2000 => "muinainen"
case Album(nimi, tekija, vuosi) => tekija + ": " + nimi + " (" + vuosi + ")"
Samaa tekniikkaa voisi soveltaa myös Palvelus
-tyyppihierarkiaamme,
jos haluamme käsitellä noita olioita match
-käskyllä. Se järjestyy
lisäämällä sana case
luokkamäärittelyihin ja Valitsematta
-olionkin
eteen.
Jos haluat tutustua aiheeseen lisää, löydät Traits-moduulin pakkauksesta
o1.service
tuunatun englanninkielisen version esimerkistämme. Esimerkissä
on hyödynnetty tapausluokkia; siihen on myös lisätty yllä mainitut
final
-sanat luokkamäärittelyihin.
Tehtävä: sukupuolia
Tässä pienessä tehtävässä mallinnamme toisen henkilötietoihin liittyvän käsitehierarkian:
piirreluokka GenderResponse
alakäsitteineen kuvaa käyttäjien vastauksia sukupuolta
kysyttäessä.
Ohjelmassa on kolme tapaa vastata: käyttäjä voi joko valita annetuista vaihtoehdoista
(Selected
), kuvata itse sukupuolensa haluamallaan tavalla (Specified
) tai jättää
vastaamatta (PreferNotToSay
). Annettuja vaihtoehtoja on kolme: Female
, Male
ja
NonBinary
.
Tässä vektorissa on esimerkki kunkinlaisesta GenderResponse
-oliosta:
Vector(NonBinary, Male, PreferNotToSay, Female, Specified("agender"))
Tarkoitus on, että tämä hierarkia määritellään yhdessä tiedostossa GenderResponse.scala
ja kattaa tyhjentävästi kaikki mahdolliset GenderResponse
-oliot, joita ohjelma käsittelee.
(Jos ohjelmaa kehitetään ja lisäyksiä tarvitaan, muutokset tulevat kyseiseen tiedostoon.)
Tyyppihierarkia on annettu osittaisena Traits-moduulin pakkauksessa o1.gender
.
Täydennä se. Koodiin on merkitty puutteet eli nämä:
Koska haluamme rajata alityypit tähän tiedostoon,
GenderResponse
- jaSelected
-piirreluokkien pitäisi olla suljettuja.NonBinary
-olion pitäisi olla vastaava kuinMale
jaFemale
, mutta se puuttuu.Specified
-luokka on annettu muttei oleGenderResponse
n alityyppi.PreferNotToSay
-yksittäisolio puuttuu.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Etukäteen määriteltyjä ilmentymiä
Johdanto
Äskeisissä esimerkeissä ilmoitimme sealed
-sanalla, että tiedämme etukäteen kaikki
alityypit, joita tietyillä piirreluokilla oli. Tilanne voi olla vielä tätäkin selvempi:
tietyntyyppisiä olioita on vain pieni joukko ja voimme luetella ne kaikki etukäteen. Emme
edes halua, että kyseisenlaisia olioita voi luoda lisää.
Esimerkiksi viikonpäiviä on tietyt seitsemän. Jos määrittelemme tyypin Weekday
, ei ole
tarkoituksenmukaista, että erilaisia viikonpäiväolioita luotaisiin lisää. Mieluummin vain
nimeäisimme tarvittavat seitsemän olioita koodiin ja sillä hyvä.
Yksi tapa tehdä tämä on suljettu piirreluokka. Siis näin:
sealed trait Weekday
object Monday extends Weekday
object Tuesday extends Weekday
object Wednesday extends Weekday
object Thursday extends Weekday
object Friday extends Weekday
object Saturday extends Weekday
object Sunday extends Weekday
Ehkä haluamme kuvata myös kuukausia. Tuolla tekniikalla se käy näin:
sealed trait Month
object January extends Month
object February extends Month
object March extends Month
object April extends Month
object May extends Month
object June extends Month
object July extends Month
object August extends Month
object September extends Month
object October extends Month
object November extends Month
object December extends Month
Tässä siis luettelemme kaikki mahdolliset tiettyä tyyppiä oleva oliot; muita ei ole.
Käy se noinkin. Mutta koska tällaiset tietotyypit ovat varsin yleisiä, monet ohjelmointikielet tarjoavat kätevämpiäkin tapoja määritellä niitä. Niin Scalakin.
Luetelmatyypit: enum
Mainitut käsitteet voi määritellä näinkin helposti:
enum Weekday:
case Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
enum Month:
case January, February, March, April, May, June, July,
August, September, October, November, December
Sanalla enum
määritellään luetelmatyyppi
(enumerated type). Se on luokka siinä missä
muutkin, mutta sillä on vain tietyt, nimetyt
ilmentymät.
Ilmentymät eli kaikki "tapaukset", jotka tästä
tietotyypistä on olemassa, luetellaan case
-sanan perässä.
Kun luetelmatyypit on noin määritelty, niitä voi käyttää näin:
val today = Weekday.Mondaytoday: Weekday = Monday val cruelest = Month.Aprilcruelest: Month = April
Yllä erikseen mainitsimme luetelman nimeltä
ennen kutakin oliota. Se onkin tarpeen, ellei sitten
poimi noita olioita käyttöön import
-käskyllä kuten
alla, minkä jälkeen pelkkä olion nimi riittää:
import Weekday.*val deadlineDay = WednesdaydeadlineDay: Weekday = Wednesday val weekend = Vector(Saturday, Sunday)weekend: Vector[Weekday] = Vector(Saturday, Sunday)
Kussakin luetelmatyypissä on eräitä muitakin käteviä työkaluja, kuten metodit
fromOrdinal
ja values
:
Month.fromOrdinal(11)res0: Month = December Weekday.valuesres1: Array[Weekday] = Array(Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday)
fromOrdinal
ottaa kokonaisluvun ja palauttaa luetelmatyypin
olion, joka on järjestyksessä niin mones. Numerointi alkaa
nollasta, joten esim. joulukuu on kuukausi numero 11.
values
palauttaa kokoelman, jonka alkioina on järjestyksessä
kaikki luetellut oliot. (Array
eli taulukko on eräs
kokoelmatyyppi; luku 12.1.)
Keksit varmaan itsekin muita tietotyyppejä, joiden arvot voi luetella samaan tapaan, kun olemme tässä luetelleet viikonpäiviä ja kuukausia.
Esimerkki jatkuu: muuttujia luetelmatyypissä
Luetelmatyypin olioillakin voi olla muuttujia ja metodeita ihan tavalliseen tapaan.
Täydennetään Month
-luetelmaamme lisäämällä kuukausia kuvaaville olioille tieto siitä,
montako päivää kyseisessä kuukaudessa on (välittämättä karkausvuosista). Kirjoitamme nyt
pidemmin:
enum Month(val days: Int):
case January extends Month(31)
case February extends Month(28)
case March extends Month(31)
case April extends Month(30)
case May extends Month(31)
case June extends Month(30)
case July extends Month(31)
case August extends Month(31)
case September extends Month(30)
case October extends Month(31)
case November extends Month(30)
case December extends Month(31)
Month
-luetelmamme ottaa nyt luontiparametrin
arvon talteen days
-ilmentymämuuttujaan. Tällainen
days
-muuttuja on kaikilla luetelluilla olioilla.
Nyt oliot eivät ole ominaisuuksiltaan identtisiä. Määrittelemme ne omina tapauksinaan ja ilmoitamme kussakin tapauksessa, mitä luontiparametrin arvoksi annetaan.
Näiden olioiden days
-muuttujaa käytetään aivan tutusti:
import Month.*April.daysres2: Int = 30 Month.values.map( _.days ).sumres3: Int = 365
Lisäesimerkki: kompassisuunnat
Olet jo käyttänyt (luvun 6.3 matopelissä) CompassDir
-tyyppiä,
jolla kuvataan pääilmansuuntia. Se on itse asiassa luetelmatyyppi,
josta on neljä ilmentymää: North
, East
, South
ja West
. Jos
haluat tutustua tuon luetelman toteutukseen, löydät sen O1Library-moduulin kansiosta o1.grid
.
Verityyppitehtävän paluu
Luvun 5.1 mallinnettiin verityyppejä. Käytimme ABO-luokittelun ja Rhesus-luokittelun yhdistelmää, ja kuvasimme kunkin verityypin merkkijonon ja totuusarvon yhdistelmänä tähän tapaan:
val myBlood = BloodType("AB", true)myBlood: o1.blood1.BloodType = AB+ val yourBlood = BloodType("A", true)yourBlood: o1.blood1.BloodType = A+ myBlood.canDonateTo(yourBlood)res4: Boolean = false
Aiemmassa versiossamme:
Oli vain yksi BloodType
-käsite, joka
tarkoitti aina yhdistelmää ABO-verityypistä
ja Rhesus-verityypistä.
Kuitenkaan näitä kahta ei aina käytetä yhdessä, ja verityyppiluokitteluja on olemassa muitakin. Lisäksi:
Luokan käyttäjä määräsi varsinaisen verityypin parametreilla. Luotimme siihen, että käyttäjä antoi mielekkäästi muotoiltuja merkkijonoja parametreiksi.
Tehdään ohjelmastamme joustavampi kuvaamalla ABO- ja Rhesus-luokittelutavat erikseen. Samalla otamme mallinnuksen välineeksi luetelmatyypit, joilla kuvaamme näitä eri luokitteluja. Tarkastellaan aluksi Rhesus-luokittelua.
Rhesus-verityypit luetelmana
Koska Rhesus-verityyppejä on pieni määrä (kaksi) ja koska nuo tyypit ovat etukäteen tiedossa, sopii luetelma tähän hyvin:
enum Rhesus(val isPositive: Boolean):
case RhPlus extends Rhesus(true)
case RhMinus extends Rhesus(false)
def isNegative = !this.isPositive
def canDonateTo(recipient: Rhesus) = this.isNegative || this == recipient
def canReceiveFrom(donor: Rhesus) = donor.canDonateTo(this)
override def toString = if this.isPositive then "+" else "-"
end Rhesus
Rhesus
-olioilla on muuttuja isPositive
, joka
saa arvonsa luontiparametrista.
Positiivista verityyppiä kuvatkoon olio RhPlus
ja
negatiivista RhMinus
. Niiden isPositive
-muuttujilla
on eri arvot. Muita Rhesus
-tyyppisiä olioita ei ole
olemassa.
Luetelmatyypissä voi olla metodeitakin. Nämä metodit
ovat kaikilla Rhesus
-tyyppisillä arvoilla — eli
kummallakin olioista RhPlus
ja RhMinus
.
Nyt Rhesus-luokittelun ominaisuuksia voi tutkia näitä olioita käyttäen:
import Rhesus.*RhPlus.canDonateTo(RhMinus)res5: Boolean = false RhMinus.canDonateTo(RhPlus)res6: Boolean = true RhMinus.canDonateTo(RhMinus)res7: Boolean = true RhMinus.isPositiveres8: Boolean = false
Tehtävä: ABO-verityypeille oma luetelma
Tässä tehtävässä toteutat ABO-luetelman samalla periaatteella, jolla toteutimme Rhesus
-tyypin edellä.
Moduulissa Blood on pakkaus o1.blood2
, josta löydät äskeisen Rhesus
-koodin. Lisää
samaan tiedostoon myös luetelmatyyppi ABO
. Sen on täytettävä seuraavat vaatimukset:
On neljä lueteltua oliota
A
,B
,AB
jaO
. Ne vastaavat ABO-luokittelun verityyppejä ja ovat ainoatABO
-tyyppiset oliot.ABO
-luetelmalla on ilmentymämuuttujaantigens
, joka saa arvonsa luontiparametrista. TämäString
-tyyppinen muuttuja tallentaa merkkijonona kyseisen verityypin sisältämät antigeenit.Kukin luetelluista olioista välittää eri merkkijonon luontiparametriksi. Tuo merkkijono kertoo, mitkä antigeenit kyseiseen veriryhmään liittyvät: "A", "B", "AB" tai "". (Viimeinen on siis tyhjä merkkijono.)
ABO
-luetelmalla on metoditcanDonateTo
jacanReceiveFrom
, jotka toimivat vastaavasti kuin luvun 5.1BloodType
-luokan ja äskeisenRhesus
-luetelman samannimiset metodit. Nämä metodit tosin tarkastelevat tyyppien yhteensopivuutta ainoastaan ABO-antigeenien perusteella eivätkä huomioi Rhesus-tekijää. Kumpikin metodeista ottaa yhdenABO
-tyyppisen parametrin.Luetelmalla on myös
toString
-metodi, joka palauttaa veriryhmän nimen. Nimi on sama kuinantigens
, paitsi ettäO
-veriryhmän nimi on "O".Sinun ei kuitenkaan tarvitse kirjoittaa luetelman koodiin tätä metodia. Luetelmat nimittäin saavat automaattisesti
toString
-metodin, joka palauttaa olion nimen. KunABO
-luetelma on muuten kunnossa, sentoString
toimii automaattisesti kuten tässä toivottiin.
Jos et muista, miten veriryhmien yhteensopivuus määrittyy, kertaa luvusta 5.1 ja joko omasta ratkaisustasi aiempaan tehtävään tai esimerkkiratkaisusta.
Tiedostosta test.scala
löytyy testiohjelma, jolla voit kokeilla koodiasi. Testikoodia
on tosin kommentoitu ulos, jotta et saisi turhia virheilmoituksia. Poista kommentit, kun
olet valmis testaamaan.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Entä luokittelujen yhdisteleminen?
Äsken erottelimme veriluokittelut ABO
ja Rhesus
, joita olimme
aiemmin käyttäneet yhdessä. Tarkoitus olisi, että niitä voisi
käyttää joustavasti erikseen tai yhdistellä keskenään tai muihin
luokitteluihin. Miten tuo yhteiskäyttö nyt sitten voisi käydä?
Yksi tapa on toteutettu luokaksi ABORh
, joka löytyy (ulos kommentoituna)
samasta tiedostosta, sekä tästä:
class ABORh(val abo: ABO, val rhesus: Rhesus):
def canDonateTo(recipient: ABORh) =
this.abo.canDonateTo(recipient.abo) && this.rhesus.canDonateTo(recipient.rhesus)
def canReceiveFrom(donor: ABORh) = donor.canDonateTo(this)
override def toString = this.abo.toString + this.rhesus.toString
end ABORh
Luokka vastaa toiminnallisuudeltaan aiempaa
BloodType
-luokkaamme. Se on rakennettu ABO-
ja Rhesus-luokittelut yhdistämällä.
Tämän luokkamme ei tarvitse turvautua siihen,
että sen käyttäjä antaa sille kelvollisia
merkkijonoja. Luokalle voi antaa vain
ABO
- ja Rhesus
-tyyppisiä arvoja eli
luetelmatyyppiemme olioita.
Syvemmälle Scalan tyyppijärjestelmään
(Tämä lisätehtävä on äskeistä verityyppitehtävää hankalampi ja edellyttää omatoimista opiskelua kurssimateriaalin ulkopuolelta. Se sopii kurssin tässä kohdassa lähinnä ennestään ohjelmoinnin parissa harrastuneille. Aloittelijoiden kannattaa ohittaa tämä, ja muutkin voivat.)
Saimme jo kuvattua Rhesus
-verityypit, ABO
-verityypit ja
niiden yhdistelmän ABORh
. Noille luokillemme oli yhteistä
canDonateTo
-metodi ja canReceiveFrom
-metodi (jolla oli jopa
identtinen toteutus) sekä ajatus siitä, että kyseessä on eräs
verityyppiluokittelu.
Määritellään yläkäsite BloodType
, joka kuvaa noiden kolmen
tyypin yhteistä yläkäsitettä. Rhesus
, ABO
ja ABORh
ovat
kaikki BloodType
jä.
Tässä ensimmäinen hahmotelma:
trait BloodType:
def canDonateTo(recipient: BloodType): Boolean
enum Rhesus extends BloodType:
def canDonateTo(recipient: Rhesus): Boolean = ???
/* Jne. */
enum ABO extends BloodType:
def canDonateTo(recipient: ABO): Boolean = ???
/* Jne. */
class ABORh extends BloodType:
def canDonateTo(recipient: ABORh): Boolean = ???
/* Jne. */
Tämä määrittely aiheuttaa ongelman, joka ilmenee
alatyypeissä Rhesus
, ABO
ja ABORh
.
Tutki, mikä virhetilanne syntyy. Pohdi, miksi niin käy. Voit
kirjoittaa koodisi o1.blood3
-pakkaukseen.
Tarkastele sitten tätä versiota:
trait BloodType[ThisSystem]:
def canDonateTo(recipient: ThisSystem): Boolean
Koodiin on lisätty tyyppiparametri.
Lue tyyppiparametreista Scalan dokumentaatiosta
ja muualta. Mitä alatyyppeihin Rhesus
, ABO
ja ABORh
pitää
nyt lisätä, jotta ne toimisivat yhteen tyyppiparametrillisen
BloodType
-yläkäsitteen kanssa? Toteuta muutokset o1.blood3
-pakkaukseen.
Äskeisestä puuttui vielä toinen metodi. Tarkoitus olisi poistaa
identtinen canReceiveFrom
alatyypeistä ja toteuttaa se
kertaalleen BloodType
-piirreluokassa. Näin:
trait BloodType[ThisSystem]:
def canDonateTo(recipient: ThisSystem): Boolean
def canReceiveFrom(donor: ThisSystem) = donor.canDonateTo(this)
Tuo versiopa ei enää toimikaan. Tutki, mikä virhetilanne syntyy. Pohdi, miksi niin käy.
Tässä versio, jossa molemmat metodit on kelvollisesti määritelty:
trait BloodType[ThisSystem <: BloodType[ThisSystem]]:
this: ThisSystem =>
def canDonateTo(recipient: ThisSystem): Boolean
def canReceiveFrom(donor: ThisSystem) = donor.canDonateTo(this)
end BloodType
Mikä tuo on? Lyhyesti sanottuna se on
tyypin yläraja (upper bound) ja tarkoittaa,
että tyyppiparametrin ThisSystem
tulee olla
jokin tyypin BloodType[ThisSystem]
alatyyppi.
Entä mikä tuo on? Lyhyesti sanottuna se on
ns. self type ja kirjaa, että this
viittaa
tässä piirreluokassa ThisSystem
-tyyppiseen olioon.
Selvitä, miten koodi ratkaisee edellisen version ongelmat. Etsi mainituista käsitteistä lisätietoja netistä tarpeen mukaan.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Yhteenvetoa
Jos halutaan, että yläkäsitteellä on vain tietyt alakäsitteet, sen voi sulkea sanalla
sealed
. Tällöin kaikki välittömät alatyypit on määriteltävä samassa tiedostossa.Sulkeminen toimii takeena, ettei yllättäviä alatyyppejä ole. Oikein käytettynä se voi tehdä koodista luettavampaa, ehkäistä ikäviä bugeja ja auttaa kääntäjää varoittamaan epäilyttävästä koodista.
Jos tietyn tyypin kukin olio on etukäteen tiedossa, voi nuo oliot määritellä luetelmatyypiksi (
enum
). Luetelmatyyppi on kuin tavallinen luokka, mutta siitä ei voi luoda mitään muita ilmentymiä kuin koodiin erikseen listatut.Luetelmien päähyödyt ovat samantapaisia kuin suljetuilla tyypeillä. Jotkin käsitteet ovat erittäin näppärästi kuvattavissa lyhyellä
enum
-määrittelyllä.
Lukuun liittyviä termejä sanastosivulla: piirreluokka, tyyppihierarkia; suljettu luokka; luetelma;
final
.
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.
Alatyyppi
Vapautettu
kuvaa sitä, että henkilö on vapautettu palveluksesta. Vapautukselle kirjataan perustelu.