Luku 8.3: Robotteja ja olemattomia arvoja

../_images/person10.png

Robottien toimintavuorot

Robottimaailman advanceTurn- ja advanceFullRound-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.isIntact then
    // TODO: call the brain's controlTurn method (if there is a brain)

Rikkinäinen tai jumittunut robottirunko jättää vuoronsa käyttämättä. Tämä tarkistus on toteutettu valmiiksi.

Se, miten ehjä 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.

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

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

// Versio 1:
def takeTurn() =
  if this.isIntact then
    this.brain.controlTurn()
// Versio 2:
def takeTurn() =
  if this.isIntact && this.brain.isDefined then
    this.brain.controlTurn()
// Versio 3:
def takeTurn() =
  if this.isIntact then
    this.brain match
      case Some(actualBrain) =>
        actualBrain.controlTurn()
// Versio 4:
def takeTurn() =
  if this.isIntact then
    this.brain match
      case Some(actualBrain) =>
        actualBrain.controlTurn()
      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 (missä suuri tarkoittakoon yli 10000) sekä ensimmäisen negatiivisen luvun. Haluamme lisäksi selvittää, ovatko nuo 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-- 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 niin, 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) => Some(loytynytIso % 2 == 0)
  case None              => None

Kuten aiempi takeTurn-toteutus, tämäkin ohjelmakoodi aika monisanainen, kun ottaa huomioon, kuinka yksinkertaisesta tarpeesta 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 niin, 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

Kun kutsumme Somelle foreach-metodia, parametrifunktio suoritetaan Some-olion sisällölle:

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 5.2 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 5.2) ja lift (luku 4.3). 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 profile: String, val language: Option[String], val metricSystem: Option[Boolean]):
  // tänne toString-metodi yms.
end Preferences

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

val test = Preferences("My preferred settings", Some("English"), None)test: Preferences = lang: English, metric: NOT SET
val test2 = 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, joihin sisältyy kieliasetus; Kuurallakin on asetukset muttei kieliasetusta; Teemu ei ole tehnyt asetuksia laisinkaan.

val tiinasPreferences = Some(Preferences("Tiina's profile", Some("Finnish"), Some(true)))tiinasPreferences: Some[Preferences] = Some(lang: Finnish, metric: true)
val kuurasPreferences = Some(Preferences("Kuura's profile", None, Some(true)))kuurasPreferences: 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ä jos 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-- Error: ... value language is not a member of Some[Preferences]

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. Käytämme taas 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, ehkä 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(Preferences("Tiina's profile", Some("Finnish"), Some(true)))tiinasPreferences: Some[Preferences] = Some(lang: Finnish, metric: true)
val kuurasPreferences = Some(Preferences("Kuura's profile", None, Some(true)))kuurasPreferences: Some[Preferences] = Some(lang: NOT SET, metric: true)
val teemusPreferences: Option[Preferences] = NoneteemusPreferences: Option[Preferences] = None
val tiinasLanguage = chooseLanguage(tiinasPreferences)tiinasLanguage: String = Finnish
val kuurasLanguage = chooseLanguage(kuurasPreferences)kuurasLanguage: String = English
val teemusLanguage = chooseLanguage(teemusPreferences)teemusLanguage: String = English

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"

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 profile: 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 ))

end Preferences

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

end Passenger

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

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

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

end Passenger

Koodi sanoo varsin suoraan sen, mistä on kyse: matkustaja voi matkustaa, mikäli hänellä on kortti, joka on voimassa.

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 robottiohjelmassa, johon nyt palaamme.

Robottien liikettä

Tehtävän kaksi ensimmäistä vaihetta teit luvussa 8.2. 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ä matchin sijaan korkeamman asteen metodeita.

Niin, ja vielä yksi varoituskyltti tähän:

Option-olioiden get-metodista

Luvun 4.4 lisämateriaalissa mainittiin, että Option-olioilla on metodi, joka on nimeltään yksinkertaisesti get. Saatat törmätä tuohon metodiin myös verkosta tietoa hakiessasi.

Metodi poimii "kääreen" sisällön mutta kaatuu ajonaikaiseen virheeseen, ellei sisältöä ole. Luvussa varoitettiin, että tätä metodia on syytä välttää ja että sen käyttö O1-tehtävissä on kielletty.

Aiempi varoitus pätee myös näihin robottitehtäviin ja yleisemminkin. Älä käytä Optionien get-metodia vaan muita opetettuja konsteja. Vaikka get voi vaikuttaa kätevältä, sen käyttö on huono tapa. Metodi rikkoo kielen tyyppiturvallisuuden ja sopii vain harvinaisiin erikoistilanteisiin.

Usein kysyttyä: Mutta mun get on iffin sisällä — ei kai se silloin oo kielletty?

Oletetaan, että Option[String]-tyyppinen muuttuja nimeltä ehkaSana. Sitä ei ole mahdotonta käyttää näin:

val tuloste = if ehkaSana.isDefined then ehkaSana.get else "ei sanaa"
println(tuloste)
if ehkaSana.isDefined && ehkaSana.get.length > 10 then
  println("löytyi pitkä sana")

Koodi "toimii", mutta älä silti kirjoita noin. Käytännössä aina on parempikin konsti. Esimerkiksi nämä ovat parempia:

val tuloste = ehkaSana.getOrElse("ei sanaa")
println(tuloste)
if ehkaSana.exists( _.length > 10 ) then
  println("löytyi pitkä sana")

Yksi näiden parempien konstien keskeinen etu on, että mahdollisia inhimillisiä virheitä tulee kitketyksi pois, kun mahdollisesti ohjelman kaatavaa metodia ei kutsuta lainkaan eikä tarvitse muistaa tarkastaa mitään ehtoja ennen sen kutsumista. Koodimme on myös kätevämpi kirjoittaa ja helpompi lukea, kunhan tähän tyyliin tottuu.

getillä on hyvissä Scala-ohjelmissa mielekkäitä käyttökohteita niukasti jos lainkaan. O1-tehtävissä ei yhtäkään. Metodi tuo ohjelmaan turhaa "riskiä" ja rikkoo tyyppiturvallisuutta ilman, että saavutetaan oikeastaan mitään.

Robottitehtävä, vaihe 3/9: 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.

    • CompassDir-olioilla on parametriton metodi clockwise, joka palauttaa 90 astetta myötäpäivään olevan ilmansuunnan. (Esim. East.clockwise on South.) Voit hyödyntää sitä.

  3. Tutki RobotBrainin dokumentaatiota ja koodia. Huomaa etenkin metodit controlTurn (jota kutsuit RobotBody-luokasta) sekä moveBody. Tähän piirreluokkaan ei vielä tarvita muutoksia. Sen sijaan seuraavaksi toteutetaan sen alatyyppi Spinbot.

  4. Spinbot-luokan moveBody-metodi ei tee mitään. Täydennä se.

  5. RobotWorld-luokasta puuttuvat metodit robotWithNextTurn, advanceTurn ja advanceFullRound. Täydennä ne. Korkeamman asteen metodeista voi tässäkin olla apua.

  6. 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. Palauta testattuasi.

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

Robottitehtävä, vaihe 4/9: liikkumisen pohjustus

RobotBody- ja RobotBrain-luokista puuttuu useita pieniä robottien ympäristöön ja liikkumiseen liittyviä metoditoteutuksia.

  1. Toteuta RobotBodyn metodi neighboringSquare.

  2. Toteuta RobotBrainin metodit isStuck, locationInFront, squareInFront, robotInFront ja advanceCarefully.

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

Jatkamme robottihanketta luvussa 9.1.

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 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) then 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 then 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 do
  jotain(arvo)

Lisätieto: Näinkin voi match-käskyyn kirjoittaa

Luvun 4.4 lisämateriaalissa esiteltiin lyhyesti match-käskyn ominaisuuksia. Yksi niistä oli, että case-tapaukseen voi liittää if-sanalla ehdon. Niinpä seuraavat saavat aikaan keskenään saman:

x match
  case Some(kaaritty) => if jotain(kaaritty) then x else None
  case None           => None
x match
  case Some(kaaritty) if jotain(kaaritty) => x
  case mikaVainMuuTapaus                  => None

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 usein käytetyt metodit.

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

Pidä mielessä myös mahdollisuus käsitellä Option-oliota for-silmukalla kuten yllä 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ää harjoitusta (ja se petollinen get-metodi)

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 kyseessä onkin None-olio. Kuten edellä jo korostettiin, tuota metodia tulee välttää, koska se aiheuttaa bugeja ja edustaa huonoa ohjelmointityyliä. Tämäkään tehtävä ei esittele get-metodia hyvänä vaihtoehtona vaan päinvastoin osoittaa, että sen, mitä getillä voi tehdä, saa tehtyä muutenkin.

Lisää vastaavia monivalintoja

if x.isDefined then Some(jotain(x.get)) else None
x.isEmpty || jotain(x.get)
x != None && jotain(x.get)
if x.isDefined then
  jotain(x.get)
if x.isDefined then x.get else None
if x.isDefined && jotain(x.get) then x else None
if x.isEmpty || jotain(x.get) then x else None
if x.isDefined then 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 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.

    • Älä käytä Option-olioiden get-metodia.

  • 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!

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.

Lisäkiitokset tähän lukuun

Joonatan Honkamaa ja Otto Seppälä tehostivat robottimaailman vuorokoodia.

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