Tämä kurssi on jo päättynyt.

Kurssin viimeisimmän version löydät täältä: O1: 2024

Luku 8.2: Robotteja ja olemattomia arvoja

Tästä sivusta:

Pääkysymyksiä: Miten käytän Option-tyyppiä kätevästi? Miten saan luvun 8.1 robottiprojektin täydennettyä toimivaksi?

Mitä käsitellään? Naitamme Optionin ja kokoelmien korkeamman asteen metodit. Työstämme robottisimulaattoria eteenpäin.

Mitä tehdään? Luetaan ja ohjelmoidaan. Lopussa läjä kokoavia monivalintoja.

Suuntaa antava työläysarvio:? Vajaat pari tuntia.

Pistearvo: B45.

Oheisprojektit: Robots.

../_images/person10.png

Robottien toimintavuorot

Robottimaailman advanceTurn- ja advanceFullTurn-metodien olisi tarkoitus pyörittää toimintavuoroa robottien välillä. Niitä avustaa RobotBody-luokan takeTurn-metodi, joka määrää robotin käyttämään yhden toimintavuoron.

Lähdetään toteuttamaan viimeksi mainittua. RobotBody-luokasta löytyy metodiaihio:

def takeTurn() = {
  if (this.isMobile) {
    // TODO: call the brain's moveBody method (if there is a brain)
  }
}
Rikkinäinen tai jumissa oleva robottirunko jättää vuoronsa käyttämättä. Tämä tarkistus on toteutettu valmiiksi.
Se, miten toimiva robotti käyttää vuoronsa, riippuu siitä, millainen aivo siihen on kytketty (jos mitään). Robotti kutsuu aivonsa metodia, mikä pitäisi toteuttaa tähän väliin.

Tämä pikkutehtävä toimii kertauksena ja johdattaa seuraaviin aiheisiin:

Tässä on neljä toteutusta takeTurn-metodille:

// Versio 1:
def takeTurn() = {
  if (this.isMobile) {
    this.brain.moveBody()
  }
}
// Versio 2:
def takeTurn() = {
  if (this.isMobile && this.brain.isDefined) {
    this.brain.moveBody()
  }
}
// Versio 3:
def takeTurn() = {
  if (this.isMobile) {
    this.brain match {
      case Some(actualBrain) =>
        actualBrain.moveBody()
    }
  }
}
// Versio 4:
def takeTurn() = {
  if (this.isMobile) {
    this.brain match {
      case Some(actualBrain) =>
        actualBrain.moveBody()
      case None =>
        // do nothing
    }
  }
}
Mikä seuraavista väittämistä pitää paikkansa?
Entä syyt edelliselle?

Olisi kiva, jos yksinkertaisen asian voisi sanoa yksinkertaisesti. Muun muassa siksi pidämme nyt tauon roboteista ja opettelemme hieman uutta. Palaamme sitten robottimaailmaan Option tanassa.

Luvunetsimisongelma

Otetaan erillinen esimerkki. Sanotaan vaikkapa, että haluamme löytää kokonaislukuvektorista ensimmäisen suuren luvun (tässä: yli 10000) ja ensimmäisen negatiivisen luvun sekä selvittää, ovatko löydetyt luvut parillisia.

Etsiminen sujuu kokoelmaolion find-metodilla. Tämä metodi palauttaa Option-tyyppisen arvon:

val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20)
val isoJosLoytyi = luvut.find( _ > 10000 )res0: Option[Int] = None
val negatiivinenJosLoytyi = luvut.find( _ < 0 )res1: Option[Int] = Some(-20)

Option[Int]-tyyppiselle oliolle eli "luvulle, joka ehkä on ehkä ei", ei voi määrittää jakojäännöstä %-operaattorilla:

negatiivinenJosLoytyi % 2 == 0<console>:10: error: value % is not a member of Option[Int]

Ongelma on siis sama kuin äsken takeTurn-metodissa: haluamme suorittaa toimenpiteen vain, jos tarvittava arvo on olemassa.

Ongelman jäsennys

On kolme eri tapausta: luku löytyi ja on parillinen, luku löytyi ja on pariton, tai lukua ei löytynyt, joten sen parillisuus on määrittelemätön. On laadittava koodi siten, että nämä kaikki kolme tapausta tulevat käsitellyiksi.

Tapauksien esittämiseen sopii tietotyyppi Option[Boolean]. Päätetäänkin siis tuottaa tuloksia näin:

  • Jos find palauttaa None, niin lukua ei löytynyt, joten tuloskin on None.
  • Jos find palauttaa Some, ja sen sisällä on parillinen luku, niin tuotetaan tuloksena Some(true).
  • Jos find palauttaa Some, ja sen sisällä ei ole parillinen luku, niin tuotetaan tuloksena Some(false).

Yksi mahdollisuus olisi käyttää match-käskyä:

Kelvollinen ratkaisu matchillä

val isoJosLoytyi = luvut.find( _ > 10000 )
val isonParillisuus = isoJosLoytyi match {
  case Some(loytynytIso) => loytynytIso % 2 == 0
  case None              => None
}

Kuten aiempi takeTurn-toteutus, tämäkin ohjelmakoodi aika monisanainen ottaen huomioon, kuinka yksinkertaisesta tarpeesta loppujen lopuksi on kysymys. Helpommallakin pääsee. Otetaan uusi näkökulma Option-luokkaan.

Option alkiokokoelmana

Option-tyyppinen olio on itse asiassa kokoelma: se sisältää jonkin määrän tietyntyyppisiä alkioita. Se on vain kokoelmaksi kovin yksinkertainen: alkioita on joko nolla tai yksi. None on nolla-alkioinen alkiokokoelma, ja Some(x) on yksialkioinen kokoelma, jossa alkiona on x.

Myös Scala-kirjastojen suunnittelijat ovat ajatelleet Option-luokkaa kokoelmatyyppinä. Luokka on nimittäin laadittu siten, että sillä on koko liuta aivan samoja metodeita kuin muillakin kokoelmilla! Niiden kutsuminen kätevöittää Option-luokan käyttöä usein kummasti.

Kokeillaan ensin vaikkapa Some-oliolla eli yksialkioisella kokoelmalla.

Some alkiokokoelmana

foreach-metodille annettu parametrifunktio suoritetaan Some-arvon sisältämälle arvolle:

val kokeilu = Some("Täs mä nyt oon")kokeilu: Some[String] = Some(Täs mä nyt oon)
kokeilu.foreach(println)Täs mä nyt oon

filter-metodi palauttaa alkuperäisen Somen tai None:

kokeilu.filter( _.length < 100 )res2: Option[String] = Some(Täs mä nyt oon)
kokeilu.filter( _.length >= 100 )res3: Option[String] = None

exists- ja forall-metodit toimivat luonnolliseen tapaan:

kokeilu.exists( _.length >= 100 )res4: Boolean = false
kokeilu.exists( _.length < 100 )res5: Boolean = true
kokeilu.forall( _.length < 100 )res6: Boolean = true

map-metodilla voi tuottaa toisen Some-olion, jossa on alkuperäisen Some-olion sisältämän arvon perusteella laskettu toinen arvo:

kokeilu.map( _.toUpperCase + "!!!" )res7: Option[String] = Some(TÄS MÄ NYT OON!!!)

None alkiokokoelmana

Kokeillaan sitten None-oliolla eli tyhjällä kokoelmalla. Kuten muillekaan tyhjille kokoelmille, foreach-metodi ei tee None-oliolle mitään:

val kokeilu2: Option[String] = Nonekokeilu2: Option[String] = None
kokeilu2.foreach(println)

filter ja map palauttavat aina None, koska minkäänlaista alkiota ei ole:

kokeilu2.filter( _.length < 100 )res8: Option[String] = None
kokeilu2.map( _.toUpperCase + "!!!" )res9: Option[String] = None

exists palauttaa vastaavasti aina false:

kokeilu2.exists( _.length >= 100 )res10: Boolean = false
kokeilu2.exists( _.length < 100 )res11: Boolean = false

forall-puolestaan palauttaa aina true, koska mikä tahansa ehto pitää paikkansa kaikille nollalle alkiolle. Tai ehkä paremmin sanoen: kun alkioita on nolla, ei ole yhtään sellaista arvoa, jolle annettu ehto ei olisi voimassa, oli ehto sitten mikä tahansa.

kokeilu2.forall( _.length >= 100 )res12: Boolean = true

Luvunetsimisongelman näppärämpi ratkaisu

Oletetaan taas annetuiksi nämä käskyt:

val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20)
val isoJosLoytyi = luvut.find( _ > 10000 )res13: Option[Int] = None
val negatiivinenJosLoytyi = luvut.find( _ < 0 )res14: Option[Int] = Some(-20)

Annetaan map-metodille parametriksi parillisuuden selvittävä funktio. Toisin sanoen käsketään: "Selvitä parillisuus jokaiselle find-metodin palauttaman Option-olion sisältämälle alkiolle."

isoJosLoytyi.map( _ % 2 == 0 )res15: Option[Boolean] = None

Nyt jos sattuu käymään niin, että Option on tyhjä, on tuloskin None kuten yllä. Jos taas Option-kääre sisältää arvon, sovelletaan parillisuudenselvitysfunktiota tuohon arvoon ja saadaan tulos Some-olion sisällä:

negatiivinenJosLoytyi.map( _ % 2 == 0 )res16: Option[Boolean] = Some(true)

map-metodia käyttämällä voimme siis huomioida kaikki kolme tapausta: "löytyi parillinen", "löytyi pariton" ja "ei löytynyt mitään".

Yllä käytettiin ensiesimerkin selkiyttämiseksi välivaiheita, mutta lyhyemminkin voi toki kirjoittaa:

luvut.find( _ > 10000 ).map( _ % 2 == 0 )res17: Option[Boolean] = None
luvut.find( _ < 0 ).map( _ % 2 == 0 )res18: Option[Boolean] = Some(true)

tempo-esimerkki

Tässä toinen esimerkki Option-olion map-metodin käytöstä. Se on eräs toteutustapa luvussa 4.5 esillä olleelle tempo-funktiolle:

def tempo(kappale: String) = kappale.split("/").lift(1).map( _.toInt ).getOrElse(120)tempo: (kappale: String)Int
tempo("cccedddfeeddc---/150")res19: Int = 150
tempo("cccedddfeeddc---")res20: Int = 120
Tässä käytämme metodeita split (luku 4.5) ja lift (luku 4.2). Edellisellä jaetaan merkkijono osiin kauttamerkin kohdalta. Jälkimmäisellä tuotetaan Option[String], joka sisältää kauttamerkin jälkeisen osan, jos sellaista oli.
lift-metodikutsun palauttama mahdollinen merkkijono saadaan muutettua mahdolliseksi kokonaisluvuksi käyttämällä metodeita map ja toInt.

Preferences-esimerkki

Tavoite: valinnaisia asetuksia käyttäjäprofiileissa

Olkoon kuvitteellisessa sovelluksessa seuraava luokka, joka kuvaa käyttäjän henkilökohtaisia asetuksia. Ohjelmassa on vain kaksi eri käyttäjäasetusta, jotka kertovat käyttäjän suosiman kielen ja sen, käytetäänkö SI-järjestelmän mittayksiköitä vai ei. Kumpikin asetuksista on valinnainen, eli käyttäjä saattaa olla ilmoittanut mieltymyksensä kyseisestä asiasta tai ei. Asia on kuvattu Option-olioilla:

class Preferences(val profileName: String, val language: Option[String], val metricSystem: Option[Boolean]) {
  // tänne toString-metodi yms.
}

Tässä pari käyttöesimerkkiä:

val test = new Preferences("My preferred settings", Some("English"), None)test: Preferences = lang: English, metric: NOT SET
val test2 = new Preferences("Some other settings", Some("Finnish"), Some(true))test2: Preferences = lang: Finnish, metric: true

Olkoon vielä niin, että käyttäjä joko on tehnyt itselleen asetusprofiilin, jota kuvaamaan on tallennettu Preferences-olio, tai sitten hän ei ole, jolloin Preferences-oliotakaan ei ole. Voimme kuvata asiaintilaa muuttujalla, jonka tyyppi on Option[Preferences]. Alla on kolme erillistä esimerkkiä: Tiina on tehnyt asetukset mukaan lukien kieliasetuksen, Tainallakin on asetukset muttei kieliasetusta, ja Teemu ei ole tehnyt asetuksia laisinkaan:

val tiinasPreferences = Some(new Preferences("Tiina's profile", Some("Finnish"), Some(true)))tiinasPreferences: Some[Preferences] = Some(lang: Finnish, metric: true)
val tainasPreferences = Some(new Preferences("Taina's profile", None, Some(true)))tainasPreferences: Some[Preferences] = Some(lang: NOT SET, metric: true)
val teemusPreferences: Option[Preferences] = NoneteemusPreferences: Option[Preferences] = None

Miten voidaan tuottaa String-tyyppinen arvo, joka kertoo, millä kielellä ohjelman käyttöliittymä tulisi esittää käyttäjälle? Halutaan, että mikäli käyttäjällä on tallennetut asetukset ja niihin on tallennettu language-tieto, niin käytetään kyseistä kieltä. Jos asetukset puuttuvat tai jos language-asetus on None, käytetään ohjelman oletuskieltä (joka olkoon vaikkapa englanti).

Hahmotellaan REPLissä.

Ratkaisun hahmottelua

Tämä voisi olla ensimmäinen yritys selvittää Tiinan suosima kieli:

tiinasPreferences.language<console>:20: error: value language is not a member of Some[Preferences]
            tiinasPreferences.language
                             ^

Näin helpolla emme sentään pääse, koska asetustiedot voivat puuttua tyystin. Some-oliolla ei ole language-ilmentymämuuttujaa, mutta sen sisällöllä on. Aivan kuin aiemmassa lukuesimerkissä, käytetään nytkin map-metodia:

tiinasPreferences.map( _.language )res21: Option[Option[String]] = Some(Some(Finnish))
Kieliasetus on kahden valinnaisuuden takana: "ehkä on asetukset ja niissä ehkä on kieliasetus". Se on siis tyyppiä Option[Option[String]].
Tiinalla on asetusolio: tiinasPreferences ei ole None vaan Some-olioon kääritty Preferences-olio. Tämän Preferences-olion language ei sekään ole None vaan Some-olioon kääritty merkkijono "Finnish". Kun map-metodilla pyydetään Preferences-olion language-muuttujan arvo, saadaan siis sisäkkäiset Some-oliot.

flatten-metodi poistaa sisäkkäisyyden:

tiinasPreferences.map( _.language ).flattenres22: Option[String] = Some(Finnish)

Toivottavasti jo tuttuun tapaan map ja flatten voidaan yhdistää flatMap-kutsuksi.

tiinasPreferences.flatMap( _.language )res23: Option[String] = Some(Finnish)

Näin siis saimme selville Tiinan kieliasetuksen Option[String]-muodossa: Some(Finnish) tarkoittaa, että kieliasetus on olemassa ja se on suomi.

Tavoitteenamme oli käyttää käyttäjän asetusta, jos sellainen on, ja englantia muutoin. getOrElse-metodi sopii tarkoitukseen:

val tiinasLanguage = tiinasPreferences.flatMap( _.language ).getOrElse("English")tiinasLanguage: String = Finnish

Tuloksena saatiin Optioniin käärimätön merkkijono, joka kertoo valitun kielen.

Ratkaisu kootusti

Abstrahoidaan äskeisestä tapauksesta funktio, joka selvittää annettujen (mahdollisesti puuttuvien) asetustietojen perusteella, mitä kieltä tulisi käyttää:

def chooseLanguage(prefs: Option[Preferences]) = prefs.flatMap( _.language ).getOrElse("English")chooseLanguage: (prefs: Option[Preferences])String

Tässä vielä kootusti kaikkien kolmen testihenkilömme asetukset ja käskyt, joilla kullekin heistä määritetään käytettävä kieli:

val tiinasPreferences = Some(new Preferences("Tiina's profile", Some("Finnish"), Some(true)))tiinasPreferences: Some[Preferences] = Some(lang: Finnish, metric: true)
val tainasPreferences = Some(new Preferences("Taina's profile", None, Some(true)))tainasPreferences: Some[Preferences] = Some(lang: NOT SET, metric: true)
val teemusPreferences: Option[Preferences] = NoneteemusPreferences: Option[Preferences] = None
val tiinasLanguage = chooseLanguage(tiinasPreferences)tiinasLanguage: String = Finnish
val tainasLanguage = chooseLanguage(tainasPreferences)tainasLanguage: String = English
val teemusLanguage = chooseLanguage(teemusPreferences)teemusLanguage: String = English

Lisäesimerkki

Jos haluat, voit katsoa myös seuraavan toString-toteutuksen. Siinä on lisäesimerkki map-metodin käytöstä Option-olioille.

class Preferences(val profileName: String,
                  val language: Option[String],
                  val metricSystem: Option[Boolean]) {

  override def toString = {
    def describe(name: String, value: Option[String]) = name + ": " + value.getOrElse("NOT SET")
    describe("lang", this.language) + ", " + describe("metric", this.metricSystem.map( _.toString ))
  }

}

Olisi tuo matchillakin mennyt mutta monimutkaisemmin

Esimerkiksi näin:

def chooseLanguage(prefs: Option[Preferences]) = {
  val languagePref = prefs match {
    case Some(existingPrefs) => existingPrefs.language
    case None                => None
  }
  languagePref match {
    case Some(pref) => pref
    case None       => "English"
  }
}

Passenger-esimerkki

Luvussa 4.4 teit luokan Passenger luokkaa TravelCard apuna käyttäen. Työvälineenä oli match-käsky, ja canTravel-metodin ratkaisu näytti tältä:

class Passenger(val name: String, val card: Option[TravelCard]) {

  def canTravel = this.card match {
    case Some(actualCard) => actualCard.isValid
    case None             => false
  }

}

Nyt tiedämme, että saman saa tehtyä näinkin:

class Passenger(val name: String, val card: Option[TravelCard]) {

  def canTravel = this.card.exists( _.isValid )

}

Yleisemmin voimme sanoa, että seuraavat koodit ajavat saman asian.

x match {
  case Some(sisalto) => jokuEhto(sisalto)
  case None          => false
}
x.exists(jokuEhto)
x viittaa tässä johonkin Option-olioon.
jokuEhto on jokin toimenpide, joka tuottaa Boolean-arvon Option-olion sisällön perusteella.

Muitakin Optionin metodeita kuin exists voi käyttää match-käskyn sijasta. Tilaisuuksia tehdä niin tarjoutuu esimerkiksi robottiprojektissa, johon nyt palaamme.

Robottien liikettä

Tehtävän kaksi ensimmäistä vaihetta teit luvussa 8.1. Seuraavat kaksi tehdään nyt.

Yleisiä vinkkejä kaikkiin robottisimulaattorimme tuleviin vaiheisiin:

Mun motto tähän tehtävään oli "Option on alkiokokoelma". Se auttoi kovasti.
Tässä totesin, että nuo Optionien exists-, forall- ja foreach-toiminnot ovat aivan ihania.

Koeta ratkaista tämän ja seuraavan luvun tehtävät käyttämättä match-käskyä Optioneiden käsittelyyn. Käytä sen sijaan esiteltyjä korkeamman asteen metodeita. (OK, jos ei meinaa millään onnistua, niin saat kyllä käyttää matchiäkin.)

Robottitehtävä, vaihe 3/8: toimintavuorot ja Spinbot

  1. Toteuta tämän luvun alussa esiin nostettu metodi takeTurn luokkaan RobotBody. Se käy yksinkertaisesti, kunhan valitset sopivan välineen Optionin käsittelyyn.
  2. RobotBody-luokasta puuttuu myös metodi spinClockwise. Toteuta se.
  3. Spinbot-luokan moveBody-metodi ei tee mitään. Täydennä se.
  4. RobotWorld-luokasta puuttuvat metodit nextRobot, advanceTurn ja advanceFullTurn. Täydennä ne. Korkeamman asteen metodeista voi tässäkin olla apua.
  5. Varmista kokeilemalla, että Spinbotit toimivat nyt. Kokeile myös rikkoa robotti käyttöliittymän kautta ja varmista, että kaikki silti toimii eli robotti ei.

Robottitehtävä, vaihe 4/8: liikkumisen rajoitukset

RobotBody-luokasta puuttuu robottien ympäristöön liittyviä metoditoteutuksia.

  1. Toteuta RobotBodyn metodit neighboringSquare, canMoveTowards ja isStuck.
  2. Palauta ratkaisusi vaiheisiin 3 ja 4 tällä lomakkeella.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Jatkamme robottiprojektia seuraavassa luvussa 8.3. Ensin kokoamme yhteen Option-tyypistä opittua.

Pieniä mutta pähkinäisiä

Kussakin seuraavista kysymyksistä on annettu koodinpätkä, ja tehtäväsi on vastata, millä lyhyemmällä koodinpätkällä sen voi korvata. Idea on samankaltainen kuin aiemmassa esimerkissä, jossa korvasimme matkustajaluokan match-käskyn exists-metodikutsulla.

Alla oletetaan, että jotain on kussakin kysymyksessä jokin sellainen lauseke, jonka tyyppi sopii juuri tuohon kysymykseen liittyvään koodinpätkään — eri kohdissa se voi siis olla eri tyyppinen. Lisäksi oletetaan, että x on jokin kyseiseen kysymykseen tyypiltään sopiva Option-luokan ilmentymä.

Vastaukset ovat loogisia ja yksiselitteisiä, eikä niitä ole mahdotonta saada selville järkeilemällä, mutta käytä REPL-kokeiluja apuna tarvittaessa. Tämä ei ole erityisen helppo tehtävä! Vinkki: eräissä kohdissa on syytä huomioida mahdollisuus, että Optionin sisällä voi olla toinen Option.

x match {
  case None           => false
  case Some(kaaritty) => jotain(kaaritty)
}

Mikä seuraavista tekee saman?

x match {
  case Some(kaaritty) =>
    jotain(kaaritty)
  case None =>
    // ei mitään
}
x match {
  case Some(kaaritty) => Some(jotain(kaaritty))
  case None           => None
}
x match {
  case Some(kaaritty) => if (jotain(kaaritty)) x else None
  case None           => None
}
x match {
  case Some(kaaritty) => jotain(kaaritty)
  case None           => true
}
x match {
  case Some(kaaritty) => x
  case None           => jotain
}
if (x.isDefined) x else jotain
x match {
  case Some(kaaritty) => kaaritty
  case None           => None
}
x.getOrElse(None)
x match {
  case Some(kaaritty) => jotain(kaaritty)
  case None           => None
}
for (arvo <- x) {
  jotain(arvo)
}

Mitä opittiin?

Erittäin moni Option-olioihin kohdistuva toimenpide on kätevästi toteutettavissa kutsumalla jotakin Option-olion metodia. Korkeamman asteen metodeita käyttämällä koodista tulee tiivistä — mutta ymmärrettävää, kunhan lukija tuntee kyseiset yleisessä käytössä olevat metodit.

Voit itse harkita, kuinka paljon haluat käyttää valintakäskyjä kuten if ja match, ja kuinka paljon korkeamman asteen metodeita, mutta molemmat toteutustavat kannattaa tuntea.

Mielessä kannattaa pitää myös mahdollisuus käsitellä Option-oliota for-silmukalla kuten yllä olevan monivalintatehtävän viimeisessä kohdassa.

Millainen roboratkaisu tuli?

Jos et vielä käyttänyt tämän luvun esittelemiä metodeita robottiohjelmassasi, tee se nyt; se on opettavaista. Selviytyisitkö kokonaan ilman match-käskyjä?

Lisää vastaavia monivalintoja

Jos edellinen ei piisannut, voit treenata Option-ajatteluasi myös seuraavassa tehtävässä, jossa samaan tapaan pyydetään valitsemaan annettua koodia vastaava korkeamman asteen metodi.

Seuraavissa koodinpätkissä on käytetty Option-olion get-metodia, joka yksinkertaisesti ottaa arvon kääreen sisältä ja kaatuu ajonaikaiseen virheeseen, jos sitä kutsuu None-oliolle. Kuten luvun 4.3 lopun vapaaehtoisessa materiaalissa kerrottiin, get-metodin käyttöä sopisi kaihtaa, koska se kerjää huolimattomuusvirhettä. Tämäkään tehtävä ei pyri esittelemään get-metodia hyvänä vaihtoehtona vaan päinvastoin osoittamaan, että sen, mitä getillä voi tehdä, saa tehtyä muutenkin.

if (x.isDefined) Some(jotain(x.get)) else None
x.isEmpty || jotain(x.get)
x != None && jotain(x.get)
if (x.isDefined) {
  jotain(x.get)
}
if (x.isDefined) x.get else None
if (x.isDefined && jotain(x.get)) x else None
if (x.isEmpty || jotain(x.get)) x else None
if (x.isDefined) jotain(x.get) else None

Lisää pohdittavaa ja lukemista

Mieti, voisiko Option-luokan käytön sijaan käyttää puskuria, johon aina lisäisi korkeintaan yhden alkion. Mitä hyötyä tai haittaa tästä olisi?

Selvitä, millainen on Scalan Either-luokka ja miten sitä voi käyttää osin samoihin tarkoituksiin kuin Optionia. Selvitä myös mitä ovat toisiinsa liittyvät luokat Try, Success ja Failure.

Voit lukea, mitä on sanottu Option-luokan mahdollisesta liikakäytöstä. Löydätkö tästä luvusta esimerkkejä, joiden koodia voisi selkiyttää toisenlaisella ratkaisulla? Siitäkin voit lukea, mikä on Null Object -suunnittelumalli ja miten se liittyy samaan teemaan.

Yhteenvetoa

  • Option-oliokin on alkiokokoelma. Se sisältää nolla tai yksi alkiota.
  • Option-oliolla on muista kokoelmista tuttuja metodeita: foreach, exists, map, flatMap jne. Niiden avulla "ehkä olemassa olevia arvoja" on helpompi käsitellä ohjelmissa.
    • match-käskykin toki toimii, mutta nämä metodit ovat usein näppärämpiä, kunhan totut niihin.
  • Myös for-silmukkaa voi käyttää Option-kokoelman läpikäymiseen.
  • Lukuun liittyviä termejä sanastosivulla: Option, kokoelma, korkeamman asteen funktio.

Palaute

Huomaathan, että tämä on henkilökohtainen osio! Vaikka olisit tehnyt lukuun liittyvät tehtävät parin kanssa, täytä palautelomake itse.

Tekijät

Tämän oppimateriaalin kehitystyössä on käytetty apuna tuhansilta opiskelijoilta kerättyä palautetta. Kiitos!

Kierrokset 1–13 ja niihin liittyvät tehtävät ja viikkokoosteet on laatinut Juha Sorva.

Kierrokset 14–20 on laatinut Otto Seppälä. Ne eivät ole julki syksyllä, mutta julkaistaan ennen kuin määräajat lähestyvät.

Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.

Tehtävien automaattisen arvioinnin ovat toteuttaneet Riku Autio, Jaakko Kantojärvi, Teemu Lehtinen, Timi Seppälä, Teemu Sirkiä ja Aleksi Vartiainen.

Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.

Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista ovat suunnitelleet Juha Sorva ja Teemu Sirkiä. Niiden teknisen toteutuksen ovat tehneet Teemu Sirkiä ja Riku Autio käyttäen Teemun toteuttamia Jsvee- ja Kelmu-työkaluja.

Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset on laatinut Juha Sorva.

O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.

Opetustapa, jossa käytämme O1Libraryn työkaluja (kuten Pic) yksinkertaiseen graafiseen ohjelmointiin on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.

Oppimisalusta A+ on luotu Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Pääkehittäjänä toimii tällä hetkellä Jaakko Kantojärvi, jonka lisäksi järjestelmää kehittävät useat tietotekniikan ja informaatioverkostojen opiskelijat.

Kurssin tämänhetkinen henkilökunta on kerrottu luvussa 1.1.

Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.

../_images/imho8.png
Palautusta lähetetään...