Luku 9.1: Robotteja ja ehdollista toistoa

../_images/person02.png

Johdanto

Luvuista 6.3 ja 7.1: Ohjelmointiongelmaan voi löytyä korkean abstraktiotason työkalu, joka sopii parahultaisesti juuri tuon ongelman ratkaisuun. Jos ei löydy, voimme tukeutua matalamman abstraktiotason välineisiin. Ne eivät välttämättä ole yhtä käteviä mutta sopivat laajemmin erilaisiin tilanteisiin; korkeamman tason työkalummekin on rakennettu matalamman tason työkaluja käyttäen. Joskus matalan tason työkalujen käyttö on perusteltua suoritustehokkuuden optimoimiseksi.

Esimerkkiohjelma

Otetaan yksinkertainen esimerkki: laadimme vuorovaikutteista ohjelmaa, jossa käyttäjältä kysytään tämän nimeä. Haluamme ohjelman tarkastavan, että käyttäjä todella syöttää vähintään yhden merkin mittaisen merkkijonon, ja toistavan kysymystä kunnes ei-tyhjä syöte saadaan.

Ohjelman on tarkoitus toimia tekstikonsolissa seuraavasti. Tässä käyttäjä ensin painaa vain kaksi kertaa Enter-näppäintä ja sitten syöttää nimensä:

Enter your name (at least one character, please):
The name is 0 characters long.
Enter your name (at least one character, please):
The name is 0 characters long.
Enter your name (at least one character, please): Juha
The name is 4 characters long.
OK. Your name is Juha. Hello!

Luvussa 7.2 jäsensimme tällaisia ongelmia ajattelemalla niitä syötteiden (laiskana) listana, jonka alkioihin kohdistetaan toimenpiteitä. Vaihtoehtoisesti voimme ajatella ohjelmaa imperatiivisesti eli peräkkäisinä käskyinä, jotka muokkaavat ohjelman tilaa vaihe vaiheelta. Muotoillaan tällainen ratkaisu ensin pseudokoodina.

var name = ""

Tarkasta, onko name-muuttujassa tyhjä merkkijono. Jos on, tee seuraavat asiat; muuten jatka niiden ohi:
    name = readLine("Enter your name (at least one character, please): ")
    val len = name.length
    println(s"The name is $len character${if name.length != 1 then "s" else ""} long.")
    Palaa takaisin kohtaan, jossa tarkastetaan, onko name tyhjä.

println(s"OK. Your name is $name. Hello!")

Pseudokoodimme muistuttaa if-käskyä sikäli, että siinä tarkastetaan ehdon paikkansapitävyys — onko nimi tyhjä? — ja tehdään sen perusteella päätöksiä siitä, mihin käskyyn edetään. Erona if-käskyyn on se, että ehdollinen osa palataan suorittamaan useita kertoja, kun vain ehto pysyy voimassa.

Toisaalta pseudokoodi muistuttaa for-käskyä sikäli, että tässäkin on kyse käskyjen toistamisesta, siis silmukasta. Kuitenkin tässä silmukka muodostuu tarkastamalla toistuvasti tiettyä ehtoa; se ei ole sidoksissa mihinkään läpikäytävään kokoelmaan kuten aiempien lukujen for-silmukoissa.

while-silmukat

Tässä Scala-toteutus pseudokoodillemme. Se löytyy myös WhileLoops-moduulista.

var name = ""
while name.isEmpty do
  name = readLine("Enter your name (at least one character, please): ")
  val len = name.length
  println(s"The name is $len character${if name.length != 1 then "s" else ""} long.")
end while
println(s"OK. Your name is $name. Hello!")

Määritellään silmukka sanalla while. Apuna on myös for-silmukoista jo tuttu sana do.

whilen ja don väliin tulee ehtolauseke aivan kuin if-käskyssä sanojen if ja then väliin. Tämän ehtolausekkeen arvo tarkastetaan aina ennen silmukan rungon suorittamista.

Silmukan päättävän loppumerkin voi jättää kirjoittamattakin. Silmukan loppuun ei myöskään tarvitse kirjoittaa mitään "palaa takaisin alkuun" -käskyä (kuten pseudokoodissamme oli). Kaikki while-silmukat palaavat aina takaisin tarkastamaan jatkamisehtoa, kun silmukan runko on suoritettu.

Silmukan runkoon kirjoitetut rivit suoritetaan, kun ehtolauseke on tarkastettu ja sen arvoksi on saatu true. Näin voi käydä useasti. Tässä tapauksessa näin käy aina, kun nimen on todettu olevan tyhjä merkkijono. Runko sisennetään tuttuun tapaan.

Esimerkin viimeinen rivi ei ole osa silmukkaa. Se suoritetaan vasta sitten, kun on while-rivillä todettu ehdon olevan false ja sillä perusteella päätetty lopettaa silmukan suorittaminen.

Yleisemmin sanoen while-silmukan voi kirjoittaa näin:

(Ensin voi olla käskyjä, jotka suoritetaan kerran ennen silmukkaa.)

while ehtolauseke do
  Yksi tai useampia käskyjä, jotka suoritetaan nolla tai useampia kertoja.
  Aina ennen suorittamista tarkastetaan yllä olevan ehdon paikkansapitävyys.
  Jos ehtolauseke on tosi, suoritetaan nämä käskyt kerran ennen uutta tarkastusta.
end while  // ei välttämätön rivi

(Täältä jatketaan ohjelman suoritusta, kunhan ehto on tarkastettu ja todettu epätodeksi.)

Toinen while-esimerkki

Tutustu seuraavaan animaatioon ja tee sen alussa pyydetty ennustus.

Mitä while-sana tarkoittaa?

Scalassa ja monessa muussa ohjelmointikielessä käytetty sana while eli "niin kauan kuin" johtaa joskus aloittelijaa harhaan. Esimerkiksi äskeisestä ohjelmakoodista voisi ajatella, että heti kun luku saavuttaa arvon 13 — joka ei ole pienempi kuin 10 — niin silmukan suoritus päättyy ja hypätään silmukan jälkeiseen ohjelmakoodiin. Ehto kuitenkin tarkastetaan vain silloin, kun koko silmukan sisältö on suoritettu läpi. Niinpä tämä ohjelmanpätkä tulostaa luvun 13 lopuksi kahdesti.

Samasta syystä ensimmäisessä esimerkissämme tuli raportoiduksi "The name is X characters long." myös käyttäjän viimeisen syötteen jälkeen.

Koodinlukutehtäviä: while

Mieti, mitä tapahtuu, kun seuraava ohjelmakoodi suoritetaan. Mitä arvoja tulosmuuttuja saa suorituksen eri vaiheissa?

var tulos = "TRO"
while tulos.length < 10 do
  tulos += "LO" * (tulos.length / 2)

Luettele allekkain kaikki merkkijonot, jotka ovat tulosmuuttujan arvoina tämän koodinpätkän suorituksen aikana. Muista myös viimeinen arvo!

Silmukan laatimisesta

Silmukkaa laatiessa pitää olla tarkkana. Jos rikot yhtäkin seuraavista silmukan laatimisen "kultaisista säännöistä", syntyy buginen ohjelma.

val sana = "kissa"
var indeksi = 0
while indeksi < sana.length do
  println("Merkki: " + sana(indeksi))
  indeksi += 1

1. Alustaminen: Alusta ohjelman tila sopivaksi ennen silmukan aloittamista. Usein tämä tarkoittaa käytännössä sitä, että yhdelle tai useammalle var-muuttujalle asetetaan alkuarvo.

2. Lopettaminen: Laadi tarkoituksenmukainen jatkamisehto, joka on siis samalla myös silmukan suorituksen lopettamisehto. Tässä indeksimuuttujan arvolle määritellään yläraja; kun se saavutetaan, on aika lopettaa.

3. Eteneminen: Varmista, että silmukan sisällä tehdään jotakin, mikä aikanaan lopettaa silmukan suorittamisen. Tässä kasvatetaan indeksimuuttujan arvoa, jotta se aikanaan saavuttaa ylärajan.

4. Tarttis tehrä jotain: Muista tietysti myös käskyt, jotka hoitavat silmukan varsinaisen tehtävän. Tässä tehtävänä on vain tulostella.

Jokaisessa kohdassa voi olla omat haasteensa tilanteesta riippuen. Eräs tyypillinen seuraus "unohduksesta" on ns. ikuinen silmukka: samoja käskyjä toistetaan "loputtomasti" eli kunnes tietokoneen resurssit loppuvat tai ohjelman suoritus muuten katkaistaan ohjelman ulkopuolelta.

Arvioi, mitä tapahtuisi, jos yllä olevasta koodista olisi unohtunut pois indeksimuuttujan arvoa kasvattava rivi. Mikä seuraavista kuvaa tilannetta parhaiten?

Entä seuraavan koodipätkän tapauksessa?

val sana = "kissa"
var indeksi = 0
var tulos = ""
while indeksi < sana.length do
  tulos += sana(indeksi).toString * (tulos.length + 1)
println(tulos)

Mihin "ikuisen" silmukan suorittaminen päättyy tai miten sen voi katkaista?

Suoritus voi päättyä suunnilleen samoilla tavoilla kuin nyrkkeilyottelukin:

  • Luovutus: Käyttäjä voi katkaista suorituksen. Se onnistuu jollakin ympäristöriippuvaisella tavalla, esimerkiksi IntelliJ’ssä punaisesta Stop-napista ja monessa komentoriviympäristössä painamalla Ctrl+C.

  • Tekninen tyrmäys: Jos ohjelma varaa aina vain enemmän ja enemmän tietokoneen muistiresursseja, sen suoritus katkeaa virheeseen, kun nämä resurssit loppuvat kesken.

  • Hylkäys: Jos palautat tällaisen ohjelman A+:aan, A+ katkaisee sen suorituksen vähän ajan kuluttua päättämällä ohjelma-ajoa vastaavan prosessin ulkoapäin.

  • Tyrmäys: Virrat pois.

Toki ensisijainen ratkaisu on ongelman ehkäiseminen eikä siihen reagointi.

Programmer Jim was heading to the store to pick up groceries.
As he was leaving the house his wife said: “While you are there, buy some milk.”
Jim never came back.

—kiitokset tämän varoittavan esimerkin välittäneelle opiskelijalle

Vapaaehtoista silmukkatreeniä

Pieni silmukkatehtävä

Luvussa 7.2 teimme tällaisen pikkuohjelman:

def report(input: String) = "The input is " + input.length + " characters long."
def inputs = LazyList.continually( readLine("Enter some text: ") )
inputs.takeWhile( _ != "please" ).map(report).foreach(println)

Toteuta samoin toimiva ohjelma while-silmukalla laiskalistan sijaan. Kirjoita task1.scalaan WhileLoops-moduulissa.

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

Toinen samantapainen harjoitus

Toteuta task2.scalaan ohjelma, joka toimii tekstikonsolissa näiden esimerkkien mukaisesti:

I will compute the squares of positive integers and discard other numbers.
To stop, just hit Enter.
Please enter the first number: 10
Its square is: 100
Another number: 0
Another number: -1
Another number: 20
Its square is: 400
Another number: 30
Its square is: 900
Another number: 0
Another number: 40
Its square is: 1600
Another number:
Done.
Number of discarded inputs: 3
I will compute the squares of positive integers and discard other numbers.
To stop, just hit Enter.
Please enter the first number: 0
Another number: 0
Another number: 0
Another number: 0
Another number:
Done.
Number of discarded inputs: 4

Tässä jo ensimmäinen syöte on tyhjä:

I will compute the squares of positive integers and discard other numbers.
To stop, just hit Enter.
Please enter the first number:
Done.
Number of discarded inputs: 0

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

while vs. korkeamman abstraktiotason työkalut

Ratkaisuja rinnakkain

Nämä kaksi koodinpätkää tekevät oleellisesti saman eli tuplailevat kokonaislukuja ja etsivät ensimmäisen, joka täyttää ehdon:

val tulos = LazyList.from(0).map( _ * 2 ).dropWhile( _ <= 20 ).head
val tulos =
  var luku = 0
  var tupla = 0
  while tupla <= 20 do
    luku += 1
    tupla = luku * 2
  end while
  tupla
end tulos

while-silmukan ehtoa vastaa LazyListiin perustuvassa toteutuksessa dropWhile-metodin parametrifunktio.

Määritellään seuraavaa esimerkkiä varten apufunktio:

def tutki(luku: Int): Boolean =
  println(s"Tutkin lukua $luku. Onkohan se yli 90?")
  luku > 90

Seuraavat kaksi koodinpätkää arpovat lukuja kunnes arpoutuu riittävän suuri luku:

LazyList.continually( Random.nextInt(100) ).map(tutki).find( _ == true )
var onIso = false
while !onIso do
  onIso = tutki(Random.nextInt(100))
end while

Tällä kertaa while-silmukan ehtoa vastaa find-metodikutsu.

Satunnaislukujen listaa käydään läpi kutsuen tutki-metodia kullekin luvulle. Näin muodostuu laiskalista totuusarvoista.

Näitä totuusarvoja muodostetaan, kunnes kohdataan alkio, joka on true. Tällöin find ei enää jatka myöhempien alkioiden tutkimista, eikä niitä edes muodosteta.

findin sijaan olisimme voineet käyttää muutakin metodia, joka pakottaa laiskalistan muodostamaan alkioita trueen asti. Esimerkiksi contains(true) toimii myös.

Mitä tapahtuisi, jos äskeisestä LazyList-esimerkistä olisi jätetty pois lopun kutsu .find( _ == true )?

Mitkä työkalut valitsisin?

while-silmukoilla voi siis tehdä samoja asioita kuin kurssin aiemmista vaiheista tutuilla korkeamman asteen metodeilla. Usein ei kannata, mutta joskus kannattaa.

Kun sovellat korkeamman asteen metodeita kokoelmiin, korostuvat — niin koodissa kuin ajatuksissakin — käsiteltävä data ja ne toimenpiteet, joita haluat ohjelman tuohon dataan kohdistavan. Näitä metodeita käyttäessäsi jätät käskyjen suoritusjärjestyksen yksityiskohdat kirjastometodien huoleksi ja keskityt ilmaisemaan ohjelman tarkoituksen.

Kun sovellat while-silmukoita, korostuu puolestaan peräjälkeinen suoritusjärjestys. Näitä silmukoita käyttäessäsi olet pikkuisen lähempänä sitä matalan abstrakiotason vaiheittaista toimintaa, joka tapahtuu tietokoneen prosessorissa, kun se ohjelmiasi käsittelee. Otat ohjelman suoritusjärjestyksen suoremmin haltuun ja määräät nimenomaisesti, mikä käsky seuraa välittömästi mitäkin toista käskyä.

Monesti while-silmukat ovat tarpeettoman yksityiskohtaisia ja kokoelmametodit kätevämpiä, luettavampia ja vähemmän alttiita virheille. Kuitenkin while-silmukoiden käyttö voi olla perusteltua esimerkiksi seuraavissa tilanteissa.

  • Toteutat algoritmia, jonka tehtävänä on muokata ohjelman tilaa vaihe vaiheelta, ja on siksi luontevaa ilmaista ohjelman suoritusjärjestys koodiin mahdollisimman suorapuheisesti. Esimerkiksi jotkin vaiheittaiset vuorovaikutukset käyttäjän kanssa tekstikonsolissa ovat tällaisia. Tämän luvun robottitehtävät (alla) ovat ehkä rajatapaus.

  • Työstät ohjelmaa, jonka on toimittava tehokkaasti (nopeasti). Olet tutkinut koodisi toiminnan huolellisesti ja todennut tarpeelliseksi optimoida tietyn osaohjelman tehokkuuden. Tilanteesta riippuen optimoinnissa saattaa olla avuksi kuvata tuon osaohjelman täsmällinen suoritusjärjestys silmukalla.

Ehtolausekkeisiin perustuva toistokäsky kuten while on tarjolla monissa ohjelmointikielissä, ja tällaisia käskyjä käytetään laajasti. Törmäät niihin varmasti toistuvasti, kun jatkat ohjelmoinnin parissa. Ne ovat ehkä liiankin käytettyjä. Joskus niitä käytetään, koska kieli ei tarjoa muita työkaluja, joskus ohjelmointikulttuurillisista tai historiallisista syistä, joskus rajallisen osaamisen vuoksi.

Tunne siis tämäkin työkalu, mutta älä suotta käytä sitä ensisijaisena ratkaisuna kaikkiin toistoa vaativiin ongelmiin. Esimerkiksi korkeamman asteen metodit hoitavat monet hommat kätevästi, kauniisti ja riittävän tehokkaasti, ja muitakin vaihtoehtoja on.

Silmukat ensiluokan kansalaisina

Luvussa 6.1 mainittiin, että ilmaisu first-class functions viittaa siihen, että ohjelmointikieli mahdollistaa funktioiden käsittelemisen kuten muidenkin arvojen: funktioita voi sijoittaa muuttujiin, välittää parametriksi, palauttaa jne. Ne ovat kielessä ensiluokan kansalaisia.

Luvussa 7.2 käsittelimme laiskalistoja, joiden avulla pystyimme luomaan etukäteen tuntemattoman mittaisia kokoelmia ja toistamaan toimenpiteitä kunnes jokin ehto täyttyy. Siis vähän kuin tämän luvun ehdollisissa silmukoissa. Laiskalistaa — toisin kuin silmukkaa — voi käsitellä kuin muutakin dataa: sille voi kutsua metodeita kuten map, jotka tuottavat toisensisältöisen listan, ja sellaisia kuten find, contains ja exists, jotka käyvät listan vain osin läpi. Siksi jotkut kutsuvat laiskalistoja "ensiluokan silmukoiksi" (first-class loops).

Laiskalistat sopivat etenkin muuttumattoman datan käsittelyyn vaikutuksettomilla metodeilla. Tilaa muuttavissa ohjelmissa on varottava, ettei suoritusjärjestys jää liian hankalaselkoiseksi; while-silmukka voi tässä suhteessa olla laiskalistaa luettavampi.

Entä for-silmukat?

for-silmukan "muoto" määrittyy epäsuorasti ja yksinkertaisesti: kunkin kierroksen jälkeen edetään kohti silmukan loppua poimimalla seuraava alkio kokoelmasta, ja jatkamisehtona on "niin kauan kuin alkioita riittää".

for-silmukat ovat siis abstraktimpia kuin ehtolausekkeisiin perustuvat while-silmukat. Scalan for-silmukka on vain erilainen tapa kirjoittaa korkeamman asteen metodikutsu, eräänlaista syntaktista sokeria.

Robotit kunnolla liikkeelle

Tehtävän neljä ensimmäistä vaihetta teit jo edellisissä luvuissa. Loput osat ovat tässä luvussa.

Robottitehtävä, vaihe 5/9: Nosebot

Toteuta liikkuva robottityyppi, luokka Nosebot:

  1. Täydennä luokan otsikkorivi parametreineen kuntoon.

  2. Toteuta yksinkertainen mayMove. (Muista override.)

  3. Lisäksi puuttuu kaksi liikkumismetodia: moveBody ja sen avuksi sopiva attemptMove. Aloita viimeksi mainitusta ja käytä sitä moveBodyn toteutuksessa. Huomaa attemptMove-metodin paluuarvo, jota voi hyödyntää.

    LazyList vai silmukat?

    moveBodyn toteutuksessa voi hyvin käyttää silmukkaa. Toisaalta sen voi myös ratkaista laiskalistaa ja korkeamman asteen metodia käyttäen. Osaatko toteuttaa metodin molemmilla eri tavoilla? (Vain yksi ratkaisu vaaditaan, mutta koeta ihmeessä molempia.)

    Vinkki LazyList-ratkaisuun: voit esimerkiksi käyttää mapiä ja findiä hieman samaan tapaan kuin yllä olevassa esimerkkikoodissa.

  4. Kokeile nenäbotteja.

    Jos bottisi etenee tuplavauhtia, vilkaise tämä

    Aika monelle on tässä tehtävässä käynyt niin, että nenäbotti liikkuu kaksi ruutua vuorossa eli liian nopeasti. Jos sinulle käy niin, etkä itse keksi, mistä on kyse, niin tästä vinkistä löytyy todennäköisin selitys.

    Mitä tämä koodi tulostaa?

    def kokeilu(i: Int): Boolean =
      println("moi")
      i > 0
    
    if kokeilu(10) then
      kokeilu(10)
    

    Vastaus: se tulostaa "moi" kahdesti. Kokeilufunktiota kutsutaan ensin if-käskyn ehdossa, jolloin tulostuu eka "moi" ja palautuu true. Paluuarvon vuoksi kutsutaan samaa funktiota uudestaan ja tulostuu taas "moi".

    Vastaavasti on saattanut käydä sinullekin, kun olet käyttänyt attemptMove-metodia.

  5. Huomaa, että koska kokonaisuus on laadittu sopivasti, oli helppoa lisätä uusi robottityyppi simulaattoriin. Oikeastaan ainoa, mikä piti kirjoittaa, on toteutus algoritmille, jolla nenäbotit valitsevat, mihin liikkuvat.

  6. Palauta ratkaisusi ennen kuin etenet seuraavaan vaiheeseen.

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

Robottitehtävä, vaihe 6/9: törmäykset

Spinbotit ja Nosebotit eivät koskaan törmää mihinkään vuoronsa aikana. Muunlaiset robotit, joita toteutat tehtävän viimeisessä vaiheessa, törmäilevät. Tässä vaiheessa pohjustat viimeistä vaihetta täydentämällä Squaren alatyyppejä.

Wall ja Floor on määritelty samassa tiedostossa Squaren kanssa: Square.scala. Täydennä ne:

  1. Wall-yksittäisolion addRobot-metodi ei tee nyt mitään paitsi ilmoittaa paluuarvollaan, ettei robotti mahtunut seinän kanssa samaan ruutuun. Täydennä sitä niin, että se rikkoo ruutuun yrittävän robotin kuten dokumentaatio määrää.

  2. Täydennä myös Floor-olioiden addRobot-metodi dokumentaation mukaiseksi. Metodin olisi nyt siis käsiteltävä myös robottien väliset törmäykset.

Robottitehtävä, vaihe 7/9: Staggerbot

Toteuta Staggerbot.

Tarvitset satunnaislukugeneraattoria (luku 3.6). Käytä sitä täsmälleen dokumentaation määräämällä tavalla: luo yksi generaattori per Staggerbot-olio ja arvo lukuja silloin ja vain silloin kun tarvitaan uusi suunta. (Ellet seuraa scaladocia tarkasti, koodisi generoi eri satunnaisluvut kuin A+:n testit odottavat. Silloin testiohjelma ei myönnä pisteitä.)

Ohjeita ja vinkkejä:

  • Yksityisten apumetodien laatiminen on sallittua ja suositeltavaa. Tekisitkö esimerkiksi suunnan arpomisesta oman metodinsa?

  • moveTowards-metodi palauttaa käyttökelpoisen arvon.

  • Jos et saa pisteitä vaikka robottisi toimii näennäisen satunnaisesti, on kyse varmaankin siitä, ettet ole luonut satunnaislukuja täsmälleen dokumentaation mukaisesti. Tarkista ainakin nämä:

    • Kun luot lukugeneraattorin (Random-olion), annatko sille siemenluvun?

    • Luotko tasan yhden lukugeneraattorin per botti? Ethän luo uutta generaattoria aina, kun moveBody-metodia kutsutaan? Tai aina kun uusi luku arvotaan?

    • Arvotko vain sen verran lukuja kuin todella tarvitaan? Eli yhden liikkumasuuntaa varten ja ehkä — vain liikkeen onnistuessa! — toisen kääntymäsuuntaa varten?

    • Arvothan luvut oikealta väliltä? Esim. nextInt(10) palauttaa luvun väliltä 0–9.

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

Harkitse yksityisten metodien määrittelemistä myös seuraavia robottityyppejä laatiessasi.

Robottitehtävä, vaihe 8/9: Lovebot

Toteuta Lovebot.

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

Robottitehtävä, vaihe 9/9: Slaybot

Toteuta Slaybot.

Tässä on jälleen mahdollisuus käyttää silmukkaa ja/tai LazyListin metodeita. Osaatko toteuttaa luokan molemmilla tavoilla?

GridPos-luokan metodeista voi olla apua; muista dokumentaatio.

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

Yhteenvetoa

  • while-silmukat ovat yleisiä monissa ohjelmointikielissä ja ohjelmissa. Niillä voi toistaa yksittäistä käskyä tai käskyjen sarjaa niin kauan kuin tietty ehtolauseke on tosi.

    • Näitä silmukoita laatiessa on muistettava järkevän alkutilan asettaminen, jatkamisehto sekä etenemiskäsky, jolla päästään kohti silmukan lopputilaa.

  • for-silmukat ja korkeamman asteen metodit ovat usein riittävän tehokkaita ja huomattavasti kätevämpiä kuin while-silmukat.

  • Lukuun liittyviä termejä sanastosivulla: silmukka, while-silmukka, iteraatio; laiskalista; abstraktiotaso.

break- ja continue-käskyt? Entä return? Muunlaiset silmukat?

Osalle lukijoista ovat tuttuja silmukoihin liittyvät käskyt, joita käytetään eräissä suosituissa ohjelmointikielissä. Heidän mielessään herää kysymys: löytyvätkö vastaavat käskyt myös Scalasta?

Alla on vastauksia. Seuraava on sivistävää lukemista myös niille kurssilaisille, jotka eivät ole aiemmin ohjelmoineet toisilla kielillä.

Ulos silmukasta kesken kaiken?

../_images/break_incarnate.png

Useissa ohjelmointikielissä on käskyjä, joilla voi katkaista silmukan joko niin, että silmukan suoritus keskeytetään kokonaan (break), tai niin, että käynnissä oleva kierros keskeytetään, mutta alkaa seuraava kierros, ellei silmukan loppua ole jo saavutettu (continue).

Scala-ohjelmointitapaan ei yleensä kuulu tällaisten käskyjen käyttö, sillä lähes kaikissa tilanteissa löytyy vähintään yhtä elegantteja ratkaisuja ilmankin. Scalan kirjastoista löytyy tästä huolimatta eräänlainen break-käsky, josta löydät lisätietoja muualta. continue-käskyä ei Scalaan ole valmiiksi toteutettu, koska sillä vielä harvemmin on sijaa hyvin laaditussa Scala-ohjelmassa.

Arvon palauttaminen silmukasta?

Monissa ohjelmointikielissä on käsky, jolla voi nimenomaisesti käskeä tietokonetta lopettamaan funktion suorituksen ja palauttamaan arvon välittömästi. Käskyn nimi on useimmiten return. Sen avulla funktion suorituksen voi katkaista esimerkiksi keskeltä silmukan läpikäyntiä, jolloin silmukankin suoritus katkeaa (vähän kuin break-käskyllä).

Scalassa on myös return-käsky, mutta sitä ei usein käytetä. On harvinaista, ettei parempaa ratkaisutapaa löytyisi kuin tuon käskyn käyttö, joskin kyseessä on osin ohjelmakoodin selkeyteen liittyvä makuasia.

Alla on yksi pieni esimerkki return-käskystä. Kyseessä on funktio, joka etsii annetusta vektorista ensimmäisen sellaisen merkkijonon, jossa on ainakin yksi merkki. (Tämä siis tekee oleellisesti saman kuin vektori.find( _.nonEmpty ), muttei itse asiassa ole aivan yhtä tehokas kuin tuo vektorien kirjastometodi.)

def ekaJokaEiOleTyhja(vektori: Vector[String]): Option[String] =
  var indeksi = 0
  while indeksi < vektori.size do
    val alkio = vektori(indeksi)
    if alkio.nonEmpty then
      return Some(alkio)  // Löytyi: lopetetaan heti käymättä läpi muita alkioita.
    indeksi += 1
  end while
  return None

return-käsky katkaisee silmukan ja koko funktion suorituksen ja palauttaa return-sanan jälkeisen lausekkeen arvon.

Metodin rungon viimeinen rivi suoritetaan vain, jos silmukan keskeltä ei poistuttu return-käskyllä.

return-käskyä käyttävälle metodille on Scalassa välttämätöntä erikseen kirjata koodiin paluuarvon tyyppi.

Tämän kurssin esimerkkiohjelmissa return-käskyä on käytetty hyvin niukasti.

Lisää return-käskystä

Monet Scala-ohjelmoijat karsastavat returnia, eivätkä käytä sitä melkein koskaan. Perustelut liittyvät usein koodin selkeyteen ja return-sanan tarpeettomuuteen.

return-käsky vaikeuttaa joskus ohjelman suoritusvaiheiden (control flow) seuraamista. Tämä haitta toteutuu erityisesti silloin, jos koodissa on sisäkkäisiä tai muuten monimutkaisia silmukoita. returniin nojautuminen saattaa myös johdatella sellaiseen epäselvään tyyliin.

Suositeltavampana ohjelmointityylinä pidetään sitä, että ohjelma jäsennetään hyvin pieniin, selkeisiin aliohjelmiin, joissa tarvetta returnille ei yleensä synny. Lisäksi esimerkiksi Scalalla voi tehdä monia asioita kauniisti käyttäen muita tekniikoita "silmukka ja return" -ratkaisumallin sijaan. Tällaisia tekniikkoja ovat kokoelman vain osin läpikäyvät korkeamman asteen funktiot (esim. find, takeWhile, exists) sekä rekursio (luku 12.2). Korkeamman asteen funktiot ja rekursio ovat erityisen yleisiä funktionaalisessa ohjelmoinnissa (luku 11.2), mutta eivät suinkaan ole sidottuja vain siihen.

Ei returnin käyttö Scalassa silti kuolemansynti ole, varsinkaan jos pidät koodisi muuten siistinä.

(Jyrkempiäkin mielipiteitä on, ja niillekin löytyy järkiperusteita. Mitä funktionaalisemmalla tyylillä ohjelmoidaan, sitä perustellumpaa on välttää returnia.)

return-rönsy: bugitarina StarCraftistä

Blogikirjoituksessaan Whose bug is this anyway?!? pelikehittäjä Patrick Wyatt ystävällisesti kertoo menneestä kommelluksesta: hän ei huomannut StarCraft-pelin koodin virhettä, jonka "olisi pitänyt olla ilmeinen".

Virhe liittyi return-käskyyn, joka katkaisee aliohjelman suorituksen tietyn ehdon toteutuessa. Kuitenkaan monta riviä alempana saman aliohjelman koodissa ei huomioida sitä, että tuota koodia ei suoriteta, jos ehto ylempänä toteutui.

Tästä opettavaisesta tarinasta on hyvä huomata, että return-käskyä seurasi samassa aliohjelmassa pitkä pötkö muita käskyjä, joiden suorittaminen riippui siitä, onko return-käsky kaukana ylempänä suoritettu vai ei. return on vaarallinen erityisesti silloin, jos ohjelmaa ei ole jaoteltu pieniin aliohjelmiin.

Lisää break-käskystä

../_images/breaking_bad.png

break-käskyä karsastavat vielä useammat kuin returnia.

breakia on kritisoitu pitkälti samanlaisin perusteluin kuin returniakin. Lisäsyytöksenä on se, että jos nyt kuitenkin halutaan katkaista silmukan suoritus ja jos koodi on laadukkaasti jäsennetty lyhyiksi aliohjelmiksi, joilla on kullakin yksi oma tehtävänsä, niin return riittää katkaisijaksi.

Jos koet tarvitsevasi break-käskyä, voit ensin miettiä, olisiko sittenkin selkeämpää muodostaa metodi, jonka suoritus katkaistaan returnilla breakin sijaan. (Sen jälkeen voit miettiä, voisitko toteuttaa senkin metodin jollakin muulla kuin returnilla.)

Lisää silmukkatyyppejä — for?

Eräissä ohjelmointikielissä on erilainen for-silmukka, jossa on varattu erilliset paikat alustuskäskyille, jatkamisehdolle ja silmukan edistämiskäskylle. Esimerkiksi Java-kielisessä ohjelmassa voi kirjoittaa tähän tapaan:

// Tämä on Javaa eikä Scalaa.
for (indeksi = 0; indeksi < merkkijono.length(); indeksi += 1) {
  // tänne toistettavia käskyjä
}

Scalaan juuri tuollaista silmukkatyyppiä ei ole määritelty. Scalan for-käsky sen sijaan taipuu moniin toisenlaisiin temppuihin, joista lisää myöhemmin.

Lisää silmukkatyyppejä — do?

Joissakin ohjelmointikielissä on myös do-sanalla alkava silmukka, joka suorittaa käskyt yhden tai useamman kerran (vrt. while-silmukka, joka suorittaa käskyt nolla kertaa tai useammin). Scalan vanhoissa versioissakin sellainen oli, mutta se on poistettu tarpeettomana. Jos välttämättä haluat kirjoittaa nyky-Scalalla silmukan, joka ei tarkista jatkamisehtoa ennen kuin silmukan sisältö on vähintään kerran suoritettu, niin seuraava while-silmukkaan perustuva "kikka" on mahdollinen:

def tulostaAinakinYksiNelio(raja: Int) =
  var luku = 1
  var nelio = 1
  while
    println(nelio)
    luku += 1
    nelio = luku * luku
    nelio <= raja
  do ()

Tuo funktio tulostaa vähintään yhden rivin, vaikkei raja olisi positiivinen. Ehtolausekkeen paikalle while- ja do-sanojen väliin on kirjoitettu kokonainen lohko koodia, joka alkaa silmukan varsinaisella sisällöllä ja päättyy jatkamisehtoon. Kullakin silmukan toistokerralla suoritetaan ensin nuo kolme muuta riviä ja lopuksi evaluoidaan neljännen rivin vertailulauseke, joka määrää, jatketaanko. do-sanan perässä, jossa silmukan runko tavallisesti olisi, on vain tyhjät sulut, joten siinä kohden ei tehdä mitään.

Tämä tyyli voi tosin hämmentää koodin lukijoita. Emme suosittele.

Palaute

../_images/be_back.png

Robotit tekevät paluun luvun 10.3 vapaaehtoisessa tehtävässä.

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