Luku 7.4: Suljettuja tyyppejä ja lueteltuja olioita

../_images/person04.png

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ä:

../_images/inheritance_palvelus.png

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

Alatyyppi Vapautettu kuvaa sitä, että henkilö on vapautettu palveluksesta. Vapautukselle kirjataan perustelu.

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ä Palvelusta 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 Palvelusta 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ä.

../_images/inheritance_gender.png

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- ja Selected-piirreluokkien pitäisi olla suljettuja.

  • NonBinary-olion pitäisi olla vastaava kuin Male ja Female, mutta se puuttuu.

  • Specified-luokka on annettu muttei ole GenderResponsen 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:

../_images/inheritance_rhesus.png
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:

../_images/inheritance_abo.png
  • On neljä lueteltua oliota A, B, AB ja O. Ne vastaavat ABO-luokittelun verityyppejä ja ovat ainoat ABO-tyyppiset oliot.

  • ABO-luetelmalla on ilmentymämuuttuja antigens, 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 metodit canDonateTo ja canReceiveFrom, jotka toimivat vastaavasti kuin luvun 5.1 BloodType-luokan ja äskeisen Rhesus-luetelman samannimiset metodit. Nämä metodit tosin tarkastelevat tyyppien yhteensopivuutta ainoastaan ABO-antigeenien perusteella eivätkä huomioi Rhesus-tekijää. Kumpikin metodeista ottaa yhden ABO-tyyppisen parametrin.

  • Luetelmalla on myös toString-metodi, joka palauttaa veriryhmän nimen. Nimi on sama kuin antigens, 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. Kun ABO-luetelma on muuten kunnossa, sen toString 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.

../_images/inheritance_aborh.png

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 BloodTypejä.

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.

a drop of ink
Palautusta lähetetään...