Luku 3.4: Valintoja

../_images/person05.png

Valitsemisen tarve

Mitä erilaisimmissa ohjelmissa on tarpeen tehdä valintoja. Esimerkiksi: Täytyy valita uudeksi suosikkikokemukseksi joko uusi kokemus tai vanha suosikki. Tai: halutaan suorittaa toiminto vain, jos käyttäjä vastaa "kyllä" eikä "ei".

On siis voitava muodostaa ohjelmaan ehtoja: vastasiko käyttäjä "kyllä"; onko kokemus parempi kuin toinen? Ehtojen muodostaminen meiltä jo onnistuukin totuusarvoja käyttäen.

Lisäksi tarvitsemme tavan, jolla voimme määrätä tietyt käskyt suoritettaviksi vain, jos ehto on voimassa. Kätevää olisi sekin, että samalla voisimme määrätä vaihtoehtoiset käskyt suoritettaviksi siinä tapauksessa, että ehto ei toteudu.

Scala tarjoaa useita eri tapoja käskyjen valinnaiseen suorittamiseen. Tässä luvussa niistä käsitellään suoraviivaisinta eli if-valintakäskyä.

if-käsky

Sanoja if ja else käyttäen voi muodostaa lausekkeita, joiden arvo riippuu siitä, toteutuuko tietty ehto evaluointihetkellä eli onko tietyn Boolean-tyyppisen lausekkeen arvo true vai ei. Perusidea on pseudokoodina tämä:

if ehto then arvo ehdon toteutuessa else arvo muutoin

Käskyn toimintaa on helppo tutkia REPLissä:

if 10 > 100 then "on isompi" else "ei ole isompi"res0: String = ei ole isompi
if 100 > 10 then "on isompi" else "ei ole isompi"res1: String = on isompi

Huomaa avainsanat if, then ja else. Ehtolauseke on if- ja then-sanojen välissä.

Ehtolausekkeen on oltava tyyppiä Boolean, jolloin sen arvoksi saadaan evaluoitaessa joko true tai false. Tässä ehtolauseke on muodostettu vertailuoperaattorilla. Kun if-käsky suoritetaan, evaluoidaan aluksi tämä ehtolauseke kertaalleen ja sen perusteella määrittyy, mitä seuraavaksi tapahtuu.

Jos ehtolausekkeen arvo on false, suoritetaan else-sanan jälkeinen ohjelmakoodi eli "else-haara". Koko if-lausekkeen arvoksi saadaan else-haaran arvo.

Jos ehtolausekkeen arvo on true, suoritetaan heti ehtolauseketta seuraava ohjelmakoodi eli "then-haara". Koko if-lausekkeen arvoksi saadaan then-haaran arvo.

Esimerkkejä

Ehtolausekkeena voi olla vertailun sijaan mikä vain Boolean-tyyppinen lauseke, vaikkapa Boolean-muuttujan nimi:

val maapalloOnLittea = falsemaapalloOnLittea: Boolean = false
if maapalloOnLittea then "Niin varmaan" else "Ihanko tosi?"res2: String = Ihanko tosi?

Tai totuusarvoliteraali true tai false (joskaan tästä ei ole liiemmin hyötyä):

if true then "Tämä valittiin" else "Tätä ei"res3: String = Tämä valittiin

Äskeiset esimerkit valitsivat kahden merkkijonolausekkeen välillä, mutta then- ja else-haaroina voi toki käyttää myös vaikkapa lukuarvoisia lausekkeita:

val luku = 100luku: Int = 100
if luku > 0 then luku * 2 else 0res4: Int = 200
if luku < 0 then luku * 2 else 0res5: Int = 0

Koko if-lausekkeen arvon tyyppi määräytyy sen mukaan, mitä valinnaisiin haaroihin kirjoitetaan.

if-käsky lausekkeena

Valintakäskyllä on siis tyyppi. Tällaista käskyä voi käyttää lausekkeena missä tahansa yhteydessä, johon sen tietotyyppi sopii. Niinpä valintakäskyn voi kirjoittaa vaikkapa parametrilausekkeeksi, tai käskyn tuottaman arvon voi sijoittaa muuttujaan. Jos valintakäsky tuottaa numeron, niin käskyä voi käyttää osana aritmeettista laskutoimitusta. Esimerkiksi seuraavat käskyt ovat mahdollisia:

println(if luku > 100 then 10 else 20)20
val valinnanTulos = if luku > 100 then 10 else 20valinnanTulos: Int = 20
(if luku > 100 then 10 else 20) * (if luku <= 100 then -luku else luku) + 1res6: Int = -1999

Viimeinen noista käskyistä on tosin jo sen verran haarakas, että on selkeämpää ottaa if-lausekkeiden arvot tilapäissäilöön muuttujiin ja vasta sitten kertoa ne keskenään. Tähän tapaan:

val ekaTulos = if luku > 100 then 10 else 20ekaTulos: Int = 20
val tokaTulos = if luku <= 100 then -luku else lukutokaTulos: Int = -100
ekaTulos * tokaTulos + 1res7: Int = -1999

Pikkutehtäviä

Suoritetaan nämä käskyt REPLissä:

val suosikki = "laama"
val elain = "kissa"
val kommentti = if elain == suosikki then "Jee" else "Jaa"

Mikä on nyt viimeksi määritellyn muuttujan arvo?

Oletetaan edelliset käskyt suoritetuiksi ja suoritetaan nyt tämä:

println(if kommentti == "Jee" then "positiivinen" else "neutraali" + " reaktio")

Kumpi seuraavista pitää paikkansa?

Jatketaan suorittamalla seuraava käsky:

println(if kommentti == "Jaa" then "neutraali" else "positiivinen" + " reaktio")

Kumpi seuraavista pitää paikkansa?

Jatketaan suorittamalla seuraava käsky:

println((if kommentti == "Jaa" then "neutraali" else "positiivinen") + " reaktio")

Kumpi seuraavista pitää paikkansa?

Jatketaan suorittamalla seuraava käsky:

println("Se oli " + if kommentti == "Jaa" then "neutraali" else "positiivinen" + " reaktio.")

Mikä seuraavista pitää paikkansa?

Jatketaan suorittamalla seuraava käsky:

println("Se oli " + (if kommentti == "Jaa" then "neutraali" else "positiivinen") + " reaktio.")

Kumpi seuraavista pitää paikkansa?

if-käskyn jakaminen riveiksi

Mainittakoon tässä välissä, että jos valintakäskystä muodostuisi muuten pitkä ja hankalalukuinen, niin sen voi jakaa usealle riville esimerkiksi näin:

val pidempiKommentti =
  if elain == suosikki then
    "Mahtavaa! Se on " + elain + ", joka on oma suosikkini."
  else
    "Ihan kiva eläinhän se on " + elain + "kin."
  end if

Monirivisessä if-käskyssä then-haara ja else-haara sisennetään syvemmälle kuin if- ja else-alkuiset rivit.

Loppuun voi vapaaehtoisesti kirjata loppumerkin. Usein loppumerkki kyllä jätetään pois, mutta sen kirjoittamisessa ei ole mitään ongelmaa, jos koet, että se selkiyttää kyseistä ohjelmaa.

Minitehtävä: kuvan kuvailu, osa 1/2

Kirjoita Miscellaneous-moduulin misc.scala-tiedostoon vaikutukseton funktio describe, joka

  • ottaa yhden, Pic-tyyppisen parametrin, ja

  • palauttaa merkkijonon "portrait", jos kuvan korkeus on leveyttä suurempi ja merkkijonon "landscape" muutoin (eli jos kuva on neliön muotoinen tai leveämpi kuin korkea).

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

Experience-luokka valmiiksi

Voimme käyttää if-valintakäskyä luokan ohjelmakoodissa. Näin mahdollistamme, että oliot voivat tehdä valintoja, kun niiden metodeita kutsutaan.

Valitseva chooseBetter-metodi

Meillä on jo luvusta 3.3 osittainen toteutus Experience-luokalle. Luokasta jäi tällöin toteuttamatta chooseBetter-metodi, joka vertailee kahta kokemusta ja palauttaa niistä arvosanaltaan paremman. Kunhan saamme metodin toteutettua, sen pitäisi toimia tähän tapaan:

val wine1 = Experience("Il Barco 2001", "ookoo", 6.69, 5)wine1: o1.goodstuff.Experience = o1.goodstuff.Experience@1b101ae
val wine2 = Experience("Tollo Rosso", "turhahko", 6.19, 3)wine2: o1.goodstuff.Experience = o1.goodstuff.Experience@233b80
val betterOfTheTwo = wine1.chooseBetter(wine2)betterOfTheTwo: o1.goodstuff.Experience = o1.goodstuff.Experience@1b101ae
betterOfTheTwo.nameres8: String = Il Barco 2001

chooseBetter-metodin ajatus on siis samankaltainen kuin max-funktion (luku 1.6), joka palauttaa kahdesta vertailemastaan luvusta suuremman.

Tässä ensin Experience-toteutus, jossa chooseBetter on pseudokoodina:

class Experience(val name: String, val description: String, val price: Double, val rating: Int):

  def valueForMoney = this.rating / this.price

  def isBetterThan(another: Experience) = this.rating > another.rating

  def chooseBetter(another: Experience) =
    Selvitä, onko tämä kokemus arvosanaltaan parempi kuin another-kokemus.
    Jos oli, palauta viittaus this-olioon, muuten palauta another-muuttujan sisältämä viittaus.

end Experience

Ja sitten metodin koodi Scalalla:

def chooseBetter(another: Experience) =
  val thisIsBetter = this.rating > another.rating
  if thisIsBetter then this else another

this-sana yksinään muodostaa lausekkeen, jonka arvo on viittaus metodia suorittavaan olioon itseensä.

chooseBetterin suoritus päättyy if-lausekkeeseen; metodi palauttaa tämän if-lausekkeen arvon. Se on viittaus metodia suorittavaan olioon, jos muuttujan thisIsBetter arvo on true, tai parametriksi saatu viittaus, jos thisIsBetter on false.

Parempi ratkaisu omaa metodia kutsumalla

Äskeinen toteutus toimii. Ehkä silti haluamme poistaa Experience-luokasta tarpeettoman toiston: nythän metodeissa isBetterThan ja chooseBetter on täsmälleen sama vertailuoperaatio this.rating > another.rating. Kokemusten paremmuus on siis määritelty kahteen kertaan eri paikoissa.

Tämä ei ole kovin eleganttia ja tekee ohjelmasta hieman hankalamman muokata. Jos esimerkiksi haluaisimme muuttaa ohjelmaa niin, että vertailu tapahtuukin hinta–laatu-suhteen eikä arvosanan perusteella, niin pitäisi muuttaa kahta kohtaa. Tämä ei ole tässä pienessä ohjelmassa kauheaa, mutta isommissa ohjelmissa toisteisuudesta seuraa pian ylläpidettävyysongelmia.

Koetetaan tehdä vähän paremmin.

Olet jo useasti nähnyt, että olio voi "lähettää itselleen viestin" eli kutsua omaa metodiaan: this.metodi(parametrit). Niinpä voimme muotoilla uuden version chooseBetter-metodista:

def chooseBetter(another: Experience) =
  val thisIsBetter = this.isBetterThan(another)
  if thisIsBetter then this else another

Experience-olio ikään kuin kysyy itseltään: "Oletko parempi kuin tämä toinen kokemus?"

Nyt chooseBetter käyttää juuri sitä tapaa vertailla paremmuutta, joka on määritelty isBetterThan-metodissa, eikä toistoa ole.

Tiiviimpi ratkaisu: metodikutsu ehtolausekkeena

Varustaudutaan vielä tiedolla, että if-käskyssä voi käyttää myös metodikutsua ehtolausekkeena, kunhan vain metodin paluuarvo on tyyppiä Boolean. Tämä tulee ilmi seuraavasta REPL-esimerkistä (jossa oletetaan wine1 ja wine2 määritellyiksi kuin edellä):

if wine1.isBetterThan(wine2) then "oli parempi" else "ei ollut parempi"res9: String = oli parempi
if wine2.isBetterThan(wine1) then "oli parempi" else "ei ollut parempi"res10: String = ei ollut parempi
if wine1.isBetterThan(wine1) then "oli parempi" else "ei ollut parempi"res11: String = ei ollut parempi

Ehtolausekkeen evaluointi tarkoittaa tässä tapauksessa metodin kutsumista; lausekkeen arvoksi saadaan metodin palauttama totuusarvo. Tämän jälkeen jatketaan paluuarvon mukaiseen haaraan.

Tältä pohjalta voimme lyhentää chooseBetter-toteutustamme hieman. Paikallista muuttujaa thisIsBetter ei välttämättä tarvita:

def chooseBetter(another: Experience) = if this.isBetterThan(another) then this else another

Experience-luokkamme on valmis. GoodStuff-moduulin toiseen keskeiseen luokkaan Category tartumme myöhemmin luvussa 4.2.

Odds-tehtävä (osa 7/9)

Palataan taas hetkeksi edellisistä luvuista tuttuun Odds-hankkeeseemme ja lisätään sinne metodi, jolla Odds-olion kuvaama tieto voidaan esittää ns. moneyline-muodossa, jota varsinkin amerikkalaiset vedonlyöntitoimistot käyttävät. Tämä hieman kummallinen esitysmuoto toimii seuraavasti:

(Alla P ja Q viittaavat lukuihin, joista todennäköisyyden fractional-muotoinen esitys P/Q muodostuu.)

Jos tapahtuman on arvioitu tapahtuvan korkeintaan 50 %:n todennäköisyydellä, sen moneyline-luku on positiivinen ja saadaan laskemalla 100 ⋅ P ∕ Q. Esimerkiksi tapausta 7/2 vastaa moneyline-luku 350, koska 100 ⋅ 7 ∕ 2 = 350. Tämän positiivisen luvun voi tulkita niin, että panostamalla 100 rahaa vedonlyöjä voi tehdä 350 rahaa voittoa sen lisäksi, että saa panoksensa takaisin. Fifty–fifty-tilannetta 1/1 vastaa moneyline-luku 100.

Jos taas todennäköisyydeksi on arvioitu yli 50 %, moneyline-luku on negatiivinen ja saadaan laskemalla -100 ⋅ Q ∕ P. Esimerkiksi tapausta 1/5 vastaa moneyline-luku -500, koska -100 ⋅ 5 ∕ 1 = -500. Tämän negatiivisen luvun voi tulkita niin, että tehdäkseen 100 rahaa voittoa vedonlyöjän on panostettava 500 rahaa.

Tehtävänanto

Laadi Odds-luokkaan metodi moneyline, joka palauttaa Odds-oliota vastaavan moneyline-luvun Int-arvona:

val norwayWin = Odds(5, 2) norwayWin: Odds = o1.odds.Odds@171c36b
norwayWin.moneylineres12: Int = 250

Lisää myös käynnistysolioon OddsTest1 rivi, joka tulostaa moneyline-luvun. Ohjelman tulosteen on nyt näytettävä tällaiselta:

Please enter the odds of an event as two integers on separate lines.
For instance, to enter the odds 5/1 (one in six chance of happening), write 5 and 1 on separate lines.
11
13
The odds you entered are:
In fractional format: 11/13
In decimal format: 1.8461538461538463
In moneyline format: -118
Event probability: 0.5416666666666666
Reverse odds: 13/11
Odds of happening twice: 407/169
Please enter the size of a bet:
200
If successful, the bettor would claim 369.2307692307692
Please enter the odds of a second event as two integers on separate lines.
10
1
The odds of both events happening are: 251/13
The odds of one or both happening are: 110/154

Yksi pieni lisäys riittää.

Ohjeita ja vinkkejä

  • Käytä if-lauseketta.

  • moneyline-metodin on tässä palautettava kokonaisluku. Jätä desimaaliosa kokonaan pois eli pyöristä aina kohti nollaa. Kokonaislukujen jakolaskuhan tekee tämän puolestasi (luku 1.3), joten helpoimmalla pääset, kun vain teet laskutoimitukset sopivassa järjestyksessä: kerro ensin, jaa sitten.

  • Se on yhdyssana, joten moneyline eikä moneyLine.

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

Apokalyptistä ohjelmointia

../_images/hazel_grouse.jpg

Selvä pyy.

Suomalaisessa kansanperinteessä Jumala rankaisi pyytä tuomiten sen vähitellen pienenemään maailman loppuun saakka. Lopun tiedetään olevan liki, kun pyy on häviävän pieni. Tätä perua on sanonta "pienenee kuin pyy maailmanlopun edellä".

Simuloidaan tilanne. Tässä on pyiden mallintamiseen sopiva luokka:

class Pyy:

  private var koko = 400
  private val peruskuva = Pic("bird.png")

  def loppuKoitti = this.koko <= 0

  def pienene() =
    if this.koko > 0 then
      this.koko = this.koko - 1

  def kuvaksi = this.peruskuva.scaleTo(this.koko)

end Pyy

Löydät tuon luokan ja sitä käyttävän käyttöliittymän Pikkusovelluksia-moduulista. Käyttöliittymä käyttää luvussa 3.1 opittuja konsteja pienentääkseen valkoista taustaa vasten näkyvää pyytä kunnes se katoaa näkyvistä.

Tehtäväsi on tutustua annettuun koodiin ja muokata käyttöliittymän makePic-metodia niin, että se vaihtaa koko kuvan mustaksi lopun koittaessa. Metodin pitää siis palauttaa

  • linnun kuva valkoista taustaa vasten (kuten annetussa koodissa) vain silloin, jos pyyn loppuKoitti-metodi palauttaa false; tai

  • täysmusta maailmanlopunKuva, jos tuo metodi palauttaa true.

Käytännössä täytyy vain lisätä metodiin yksi if-lauseke.

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

Tilaan vaikuttaminen if-käskyssä

if-käskyn haarat voivat sisältää myös käskyjä, joilla on tilaa muuttavia vaikutuksia (luku 1.6) kuten tulostamista, muuttujien arvon vaihtamista yms.

Tulostava if-käsky

if luku > 0 then
  println("Luku on positiivinen.")
  println("Tarkemmin sanoen se on: " + luku)
else
  println("Kyseessä ei ole positiivinen luku.")
println("Olen puhunut.")

Sisennetyssä haarassa voi olla useitakin käskyjä, jotka suoritetaan peräjälkeen, mikäli ehtolausekkeen arvo on sopiva.

Huomaa: Esimerkin viimeinen tulostuskäsky ei kuulu if-käskyyn vaan on sen perässä. Vaikka edeltä on loppumerkki end if jätetty pois, asia näkyy siitä, että rivi on sisentämätön. Tuo tulostuskäsky suoritetaan siis ehtolausekkeen arvosta riippumatta, kunhan ensin on suoritettu jompikumpi yllä olevista "haaroista". (Jos viimeinenkin rivi olisi sisennetty, se suoritettaisiin vain mikäli luku ei ole positiivinen.)

Seuraavat animaatiot kuvaavat esimerkkikoodin toiminnan ensin positiivisella ja sitten negatiivisella lukuarvolla.

Sama yleisemmin

Aiemmin tässä luvussa käytimme if-käskyä lausekkeiden eli "ilmaisujen, joilla on arvo" muodostamiseen. Valitsimme if-käskyllä kahden arvon välillä. Äskeisessä animoidussa koodissa puolestaan käytettiin if-käskyä valitsemaan tiettyjen tilaa muuttavien vaikutusten välillä. Tässä vielä pseudokoodi, joka kuvaa tuon yleisen ajatuksen:

if ehto then
  käskyjä, jotka suoritetaan, jos ehtolausekkeen arvo oli tosi
else
  käskyjä, jotka suoritetaan, jos ehtolausekkeen arvo oli epätosi
end if   // voi jättää pois

Kun if-käskyä käytetään tilan muuttamiseen, koodi on tapana rivittää ja sisentää yllä esitetyllä tavalla riippumatta siitä, montako riviä kumpaankin "haaraan" tulee. Näin teemme tälläkin kurssilla, kuten kurssin tyyliopaskin kertoo.

Unit if-käskyn arvona

Vaikka seuraava sijoituskäsky ei olekaan kovin fiksu, on sitä hyvä pohtia hetki:

val turhaTulos =
  if luku > 1000 then
    println("Aika iso")
  else
    println("Ei niin iso")Ei niin iso
println(turhaTulos)()

Tässä sijoituskäskyn osana käytetyllä if-käskyllä ei ole merkityksellistä arvoa. Koodi kyllä tulostaa lukumuuttujan arvon perusteella jommankumman tekstin, mutta se ei sijoita muuttujaan turhaTulos mitään järkevää vaan vain sisällöttömän Unit-arvon (joka näkyy tulosteessa tyhjinä sulkeina). Näin tapahtuu siksi, että if-käskyn haarat päättyvät tulostuskäskyyn, joka ei palauta merkityksellistä arvoa. Tällaista if-käskyä on siis turha käyttää muuttujasijoituksessa.

Voi olla sivistävää vielä verrata äskeistä koodia näihin:

val tulos = if luku > 1000 then "Aika iso" else "Ei niin iso"tulos: String = Ei niin iso
println(tulos)Ei niin iso
println(if luku > 1000 then "Aika iso" else "Ei niin iso")Ei niin iso

if ilman elseä

Kun käytämme if-käskyä ohjelman tilaan vaikuttamiseen, ei else-haaraa välttämättä edes tarvita. Usein haluamme suorittaa jonkin käskyn tai käskyt vain ehdon täyttyessä, mutta muuten ei tehdä mitään.

Tietysti voisimme tehdä jotain tällaista:

if ehto then
  käskyjä, jotka suoritetaan, jos ehto oli tosi
else
  ()
end if

Tyhjä else-haara ei tee mitään. Mutta sitä ei edes tarvitse kirjoittaa; voimme jättää kaiken tämän pois.

Jos else-haaraa ei ole ja ehtolausekkeen arvoksi tulee false, niin loput if-käskyn sisältö (then-haara) yksinkertaisesti ohitetaan:

if ehto then
  käskyjä, jotka suoritetaan, jos ehto oli tosi, ja ohitetaan muutoin
end if   // tämä rivi sopii jättää pois

Esimerkki:

if luku != 0 then
  println("Osamäärä on: " + 1000 / luku)
println("Loppu.")

Tämä if-käsky on vaikutuksellinen (se tulostaa), ja siksi rivitämme ja sisennämme, vaikka haarassa on vain yksi rivi.

Jälkimmäinen, sisentämätön tulostuskäsky ei kuulu if-käskyyn vaan on sen perässä. Tämä koodinpätkä siis tulostaa lopuksi "Loppu." riippumatta siitä, oliko luku-muuttujan arvo nolla vai ei. Mikäli oli, ei tämä koodi muuta tulostakaan.

Voit katsoa tämänkin koodin suorituksesta lyhyen animaation, jos haluat:

Pikkutehtävä

Hiljenny hetkeksi tämän koodin äärelle.

var first = 20
var second = 10

if first < second then
  first = first / 2

if first < 2 * second then
  first = first * 2
  second = second / 2
else
  first = first + 1
  second = second - 1

val theyAreTheSame = (first == second)

if theyAreTheSame then
  first = first + 1

println(s"$first $second")

Mitä tapahtuu missäkin vaiheessa, kun koodi suoritetaan (kerran)?

Montako kertaa koko koodissa yhteensä evaluoidaan jonkin if-käskyn ehtolauseke (eli Boolean-tyyppinen lauseke, joka on kirjoitettu sanojen if ja then väliin)?

Montako kertaa yhteensä if-käskyn ehtolausekkeen arvoksi saadaan true?

Montako kertaa päädytään suorittamaan keskimmäisen if-käskyn else-haara?

Mitä viimeinen rivi tulostaa?

FlappyBug-tehtävä (osa 12/17: hienosäätöä)

Tehtävänanto

Lisää FlappyBug-ohjelmaan kaksi if-käskyä niin, että:

  1. Ötökkä ponnahtaa ylöspäin vain, jos painettu näppäin on välilyönti, eikä millä tahansa näppäimellä kuten tähän asti.

  2. Ötökän putoamisnopeus kiihtyy (eli yVelocity-ilmentymämuuttujan arvo kasvaa) vain, jos ötökkä on ilmassa eli maanpinnan yläpuolella.

Maahan pudottuaan ötökän pitää pystyä edelleen räpyttelemään sieltä ylös.

Maanpinta on samassa y-koordinaatissa 350 kuin tehtäväsarjan aiemmissakin vaiheissa.

Ohjeita ja vinkkejä

  • Lisäykset tulevat FlappyBugApp-ohjelman onKeyDown-metodiin ja Bug-luokan fall-metodiin. Molemmat olivat esillä, kun peliä muokattiin luvussa 3.1.

  • Aivan kuin aiemmissakin Flappy-tehtävissä, fall-metodin tulee ensin kasvattaa kiihtyvyyttä (jos maanpinnan yläpuolella ennen liikettä) ja vasta sitten siirtää ötökkää.

  • Tapahtumankäsittelijämetodi onKeyDown saa parametriksi tiedon siitä, mitä näppäintä painettiin. Voit verrata tätä arvoa yhtäsuuruusoperaattorilla == arvoon Key.Space, joka on välilyöntinäppäintä tarkoittava vakioarvo.

  • Et tarvitse else-haaroja.

  • Varo estämästä ötökän liikettä kokonaan sen pudottua maahan. Vain alaspäin kiihdytys lakkaa.

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

if-käskyjen yhdistelemisestä

Pohjustava koodinlukutehtävä

Tässä pieni funktio. Varoitus: se on huonosti kirjoitettu.

def negaPosi(luku: Int) =
  if luku < 0 then "nega" else "ei-nega"
  if luku > 0 then "posi" else "ei-posi"

Minkä merkkijonon tuo funktio palauttaa, jos sitä kutsutaan parametriarvolla 10?

Entä jos luku on 0?

Entä jos luku on -10?

else if -ketjutus

Jos haluat valita useammasta kuin kahdesta vaihtoehdosta, voit kirjoittaa if-käskyn else-haaraan toisen if-käskyn:

val kuvaus = if luku < 0 then "nega" else if luku > 0 then "posi" else "nolla"

Sulkeet voivat selkiyttää käskyä hieman:

val kuvaus = if luku < 0 then "nega" else (if luku > 0 then "posi" else "nolla")

Parhaiten monihaaraisuus ehkä näkyy rivittämällä ja sisentämällä esimerkiksi näin:

val kuvaus =
  if luku < 0 then
    "nega"
  else if luku > 0 then
    "posi"
  else
    "nolla"

Tällainen else if-ketjuttaminen toki toimii myös, kun valitaan vaikutuksellisten käskyjen välillä:

if luku < 0 then
  println("Luku on negatiivinen.")
else if luku > 0 then
  println("Luku on positiivinen.")
else
  println("Luku on nolla.")

Minitehtävä: kuvan kuvailu, osa 2/2

Muokkaa ylemmässä minitehtävässä misc.scalaan laatimaasi describe-funktiota niin, että se palauttaa

  • merkkijonon "portrait", jos kuvan korkeus on leveyttä suurempi,

  • merkkijonon "landscape", jos kuvan leveys on korkeutta suurempi, ja

  • merkkijonon "square", jos kuvan leveys on prikulleen sama kuin sen korkeus.

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

if-käskyjä sisäkkäin

if-käskyjä voi muutenkin kirjoittaa sisäkkäin. Tällöin pitää olla tarkkana sen kanssa, mihin if-käskyyn kukin else-haara liittyy. Tutustu asiaan seuraavan REPL-esimerkin kautta:

val luku = 100luku: Int = 100
if luku > 0 then
  println("On positiivinen.")
  if luku > 1000 then
    println("On yli tuhat.")
  else
    println("On positiivinen muttei yli tuhat.")
  end if
end ifOn positiivinen.
On positiivinen muttei yli tuhat.

Luvun ollessa 100 suoritetaan ulomman if-käskyn sisältö, johon sisältyy ensimmäinen tulostuskäsky.

Tuohon then-haaraan sisältyy myös toinen if-käsky. Koska tämän sisemmän if-käskyn ehto ei toteudu, päädytään sen else-osaan.

Sisemmän if-käskyn haarat ovat vielä sisennetymmät.

Kun sisennykset ovat kunnossa, if-sana, else-sana ja (valinnainen) loppumerkki ovat pystysuorassa linjassa keskenään. Tämä pätee sekä sisemmässä...

... että ulommassa valintakäskyssä. Ulommassa käskyssä ei tässä esimerkissämme ole else-haaraa ollenkaan. Loppumerkin voisi jälleen kerran jättää pois, mutta ehkäpä se selkiyttää koodia, jossa on tällaisia sisäkkäisiä rakenteita.

Äskeisessä esimerkissä siis else-sana liittyi "sisempään" if-käskyyn, joka oli sisennetty sen kanssa samalle tasolle. Tuo else-osa suoritettiin nimenomaan siksi, että ulomman käskyn ehto toteutui mutta sisemmän ei. Jos haluamme liittää elsen ulompaan if-käskyyn, se käy siirtämällä rivejä ja muuttamalla sisennyksiä:

if luku > 0 then
  println("On positiivinen.")
  if luku > 1000 then
    println("On yli tuhat.")
  end if
else
  println("On nolla tai negatiivinen.")
end ifOn positiivinen.

Nyt ulommalla if-käskyllä on kaksi haaraa, ja else-haara suoritettaisiin vain, jos luku ei olisi positiivinen. Käskyn kukin avainsana on sisennetty samalle tasolle.

Sisemmällä if-käskyllä ei tässä esimerkissä ole else-haaraa lainkaan. Tulosterivejä tulee vain yksi.

Sisäkkäisyystehtävä

Kiinnitä seuraavaa koodinpätkää lukiessasi huomiota sisennyksiin.

Esimerkissä käytetään luvun 1.7 esittelemää modulo-operaattoria % parillisuuden selvittämiseen.

if number > 0 then
  if number % 2 == 0 then
    println("A")
  else
    println("B")
  println("C")
println("D")

if number > 0 then
  if number % 2 == 0 then
    println("E")
  println("F")
else
  println("G")
println("H")

Kirjoita tähän ne kirjaimet, jotka tulostuvat, kun tuo koodi suoritetaan number-muuttujan arvon ollessa 5. (Vain kirjaimet, kiitos; esim. ACFG, jos se olisi oikein.)

Kirjoita tähän ne kirjaimet, jotka tulostuvat, kun tuo koodi suoritetaan number-muuttujan arvon ollessa 6.

Kirjoita tähän ne kirjaimet, jotka tulostuvat, kun tuo koodi suoritetaan number-muuttujan arvon ollessa -5.

Kirjoita tähän ne kirjaimet, jotka tulostuvat, kun tuo koodi suoritetaan number-muuttujan arvon ollessa -6.

Odds-tehtävä (osa 8/9)

Tehtävänanto

Luvun 3.3 OddsTest2-käynnistysolio tulostaa eräitä tietoja kahdesta Odds-oliosta. Muuta sitä:

  1. Poista isLikely- ja isLikelierThan-metodien paluuarvoja tulostavat println-käskyt. (Siis ne, joissa tuloste alkaa The-sanalla.) Tämä siksi, että tarkoitus on nyt raportoida Odds-olioiden tietoja hieman toisin kuin ennen.

  2. Nyt laadittavan ohjelman on tutkittava, onko ensimmäinen syötetty Odds todennäköisempi kuin toinen ja tulostettava sen perusteella jompikumpi seuraavista lauseista: The first event is likelier than the second. tai The first event is not likelier than the second.

  3. Tämän jälkeen ohjelman on tulostettava Each of the events is odds-on to happen., mikäli molemmat tapahtumat ovat todennäköisiä (eli isLikely). Jos kumpi tahansa tapahtumista ei ole todennäköinen, ei tässä kohtaa tulosteta mitään.

    • Kuten aiemmin määrittelimme, tapahtuma on todennäköinen, jos se toteutuu luultavammin kuin ei toteudu eli jos sen isLikely-metodi palauttaa true.

  4. Thank you -alkuisen lopputervehdyksen on edelleen tulostuttava kaikissa tapauksissa.

Ohjelman tulosteen pitää siis vastata täsmälleen näitä ajoesimerkkejä:

Please enter the odds of the first event as two integers on separate lines.
5
1
Please enter the odds of the second event as two integers on separate lines.
1
2
The first event is not likelier than the second.
Thank you for using OddsTest2. Please come back often. Have a nice day!
Please enter the odds of the first event as two integers on separate lines.
1
1
Please enter the odds of the second event as two integers on separate lines.
2
1
The first event is likelier than the second.
Thank you for using OddsTest2. Please come back often. Have a nice day!
Please enter the odds of the first event as two integers on separate lines.
1
2
Please enter the odds of the second event as two integers on separate lines.
2
3
The first event is likelier than the second.
Each of the events is odds-on to happen.
Thank you for using OddsTest2. Please come back often. Have a nice day!

Ohjeita ja vinkkejä

  • Käytä if-käskyjä.

  • Selvittäessäsi, ovatko molemmat tapahtumista todennäköisiä, voit kirjoittaa kaksi if-käskyä sisäkkäin. Vaihtoehtoisesti voit kurkistaa jo lukuun 5.1 ja noukkia sieltä toisen keinon kahden ehdon yhdistämiseen.

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

Poissulkevuudesta, tyhjentävyydestä ja tyylistäkin

Päätetään luku koodinlukuharjoitukseen, joka nostaa esiin myös ohjelmointityyliin liittyviä asioita. (Niin, ja tästä voi muuten kuitata itselleen ensimmäiset B-pisteet.)

Alla olevat kysymykset kytkeytyvät raportoi-nimiseen funktioon, jonka tulisi tutkia annettua lukua ja muodostaa rivi tulostetta sen perusteella. Tässä esimerkki halutuista tulosteista REPLissä:

raportoi(40)Aikuinen
raportoi(80)Vanhus
raportoi(15)Lapsi

Funktio on siis vaikutuksellinen, ja sen pitäisi tulostaa täsmälleen yksi seuraavista:

  • "Aikuinen" mikäli luku on 18 tai yli mutta alle 70.

  • "Vanhus" mikäli luku on 70 tai yli.

  • "Lapsi" mikäli luku on alle 18. (Myös jos luku on negatiivinen; ei välitetä nyt siitä.)

Tutustu tähän toteutukseen:

// Versio 1
def raportoi(ika: Int) =
  if ika >= 70 then
    println("Vanhus")
  else if ika >= 18 then
    println("Aikuinen")
  else
    println("Lapsi")

Toimiiko tuo koodi kuten haluttiin?

Tässäpä vielä kuusi lisätoteutusta funktiollemme — versiot 2–7. Kaikki näistä ovat ajokuntoista Scalaa, mutta vain osa niistä tuottaa halutun tulosteen kaikissa tapauksissa. Mitkä? (Ylimääräistä tulostetta ei sallita.)

// Versio 2
def raportoi(ika: Int) =
  if ika >= 70 then
    println("Vanhus")
  else if ika >= 18 then
    println("Aikuinen")
  else if ika < 18 then
    println("Lapsi")
// Versio 3
def raportoi(ika: Int) =
  if ika >= 70 then
    println("Vanhus")
  if ika >= 18 then
    println("Aikuinen")
  else if ika < 18 then
    println("Lapsi")
// Versio 4
def raportoi(ika: Int) =
  if ika >= 70 then
    println("Vanhus")
  if ika >= 18 then
    println("Aikuinen")
  else
    println("Lapsi")
// Versio 5
def raportoi(ika: Int) =
  if ika >= 70 then
    println("Vanhus")
  else if ika >= 18 then
    println("Aikuinen")
  if ika < 18 then
    println("Lapsi")
// Versio 6
def raportoi(ika: Int) =
  if ika >= 70 then
    println("Vanhus")
  if ika >= 18 then
    println("Aikuinen")
  if ika < 18 then
    println("Lapsi")
// Versio 7
def raportoi(ika: Int) =
  if ika >= 18 then
    println("Aikuinen")
  else if ika >= 70 then
    println("Vanhus")
  else
    println("Lapsi")

Nyt kun tiedät, mitkä noista toteutuksista toimivat oikein, tarkastellaan hieman ohjelmointityyliä: mikä toimivista toteutuksista on helpoin lukea; minkä toiminnasta on helpoin tehdä päätelmiä?

Aloittelijoilla voi olla eri mieltymykset kuin kokeneilla, ja jossain määrin tämä on makuasiakin. Joka tapauksessa: kun kokeneet ohjelmoijat kirjoittavat vaikutuksellisia if-käskyjä (jollaisia oli leluesimerkissämmekin), he tapaavat arvostaa näitä piirteitä:

  • Poissulkevuus kirjataan koodiin: Kaksi haaraa ovat toisensa poissulkevat, mikäli on mahdotonta, että kumpikin haaroista suoritettaisiin. Kun näin on, kirjaamme sen koodiin (elsellä). Näin lukijan helpompi hahmottaa eri tapaukset ja niiden suhteet toisiinsa.

  • Tyhjentävyys kirjataan koodiin: Jos haarat (kaksi tai useampi) yhdessä kattavat kaikki mahdolliset tapaukset, merkitsemme sen koodiin päättämällä if-ketjun "puhtaaseen" else-osioon, jossa ei ole ifiä perässä. Tuo viimeinen haara kattaa kaikki muut tapaukset; lukijan ei tarvitse huomioida mahdollisuutta, että yksikään haaroista ei tulisi valituksi.

  • Ei turhaa toistoa (DRY-periaate): Vältämme tutkimasta samaa ehtoa useasti. Vältämme myös tutkimasta sekä ehtoa (kuten x > 0) että sen käänteistapausta (kuten x <= 0).

Funktiomme tavoitteena oli, että kaikki kolme haaraa ("Vanhus", "Aikuinen", "Lapsi") ovat toisensa poissulkevat: ei ole mahdollista, että useampi kuin yksi niistä suoritetaan. Lisäksi nuo haarat ovat tyhjentävät: ei ole mahdollista, että yhtäkään niistä ei suoritettaisi.

Missä toimivista toteutuksistamme mainitut tyyliseikat toteutuvat parhaiten? (Arvaa, jos et ole varma.)

Entä jos tavoitteemme onkin vähän toisenlainen? Haluammekin funktion raportoivan i’istä näin:

raportoi(40)Aikuinen
raportoi(80)Vanhus
Aikuinen
raportoi(15)Lapsi

Tavoite on siis sama, paitsi että vanhusiän kohdalla pitäisi tulostaa sekä "Vanhus" että "Aikuinen". Toisin sanoen "Vanhus"-haara ja "Aikuinen"-haara eivät ole toisensa poissulkevat. "Lapsi"-haara on edelleen poissulkeva kummankin niistä kanssa. Nuo kolme haaraa yhdessä edelleen kattavat kaikki tapaukset tyhjentävästi.

Mitkä seitsemästä jo esitetystä toteutukseta toimivat tämän päivitetyn tavoitteen mukaisesti?

Vielä noista raportoi-versioista

Äskeisissä kysymyksissä otettiin annettuna, että joka haarassa oli println-käsky ja if-käskymme olivat siis vaikutuksellisia. Näin ei tarvitsisi olla. Alkuperäisen funktiomme voisi mainiosti toteuttaa vaikutuksettomalla if-lausekkeella, joka valitsee arvokseen merkkijonon "Vanhus", "Aikuinen" tai "Lapsi". Tämän jälkeen tarvitaan vain yksi println-käsky, joka tulostaa valitun merkkijonon.

Osaatko itse muotoilla tuollaisen version koodista?

Tällainen ratkaisu erottaa siististi toisistaan funktion kaksi tehtävää: valitsemisen ja tulostamisen. Se on myös aavistuksen enemmän DRY, koska tuloksen raportoivaa println-käskyä ei kirjoiteta useasti.

Yhteenvetoa

  • if-valintakäskyllä voi valita kumpi kahdesta käskystä tai käskyjen sarjasta suoritetaan.

  • Valintakäskyllä voi myös määrätä tiettyjä osia ohjelmakoodista valinnaisiksi. Ne suoritetaan vain, jos tietty ehto on voimassa.

  • Mitä tahansa Boolean-tyyppistä lauseketta voi käyttää valintakäskyn ehtona.

  • if-käskyjä voi yhdistellä laittamalla niitä peräkkäin tai sisäkkäin.

    • Yhdistä käskyt huolella varmistaaksesi paitsi oikean toiminnan myös koodin helppolukuisuuden. Tässä else-haarat ovat suureksi avuksi.

  • Lukuun liittyviä termejä sanastosivulla: if; totuusarvo; lauseke; DRY.

Lisätehtävä: tarkkuuspeli

Kirjoita kokonaan uusi ohjelma, jossa käyttäjän pitää yrittää osua hiiren klikkauksella keskelle ikkunaa. Tämä yksinkertainen peli päättyy, jos käyttäjä onnistuu näpäyttämään esim. kolmen pikselin etäisyydelle ikkunan keskikohdasta; tällöin ruudulle ilmestyy jokin valitsemasi kuva merkiksi onnistumisesta.

Voit luoda aihealueen malliksi yksinkertaisen olion, joka pitää Boolean-tyyppisessä muuttujassa kirjaa siitä, onko peli päättynyt. Tarvitset myös käyttöliittymän, joka tekee pelitilanteen näkyväksi.

Tästä vapaamuotoisesta lisätehtävästä ei ole tarjolla automaattista palautetta.

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