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

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

Luku 8.3: Robotteja ja ehdollista toistoa

Tästä sivusta:

Pääkysymyksiä: Miten saan toistettua käskyjä, jos mikään korkeamman asteen metodeista ei ole tarkoitukseen riittävän kätevä tai tehokas? Olen kuullut while-silmukoista muissa kielissä — mitä ne ovat ja onko niitä Scalassakin? Tehtäisiinkö lisää robottityyppejä?

Mitä käsitellään? Imperatiivisen ohjelmoinnin klassikkotyökaluja: do- ja while-silmukat. Robottisimulaattorin luokkahierarkiaa. Myös virroista opitaan toivottavasti jotain.

Mitä tehdään? Ensin luetaan, sitten ohjelmoidaan.

Suuntaa antava työläysarvio:? Yksi kurssin työläimmistä luvuista. Luvun teksteihin ja robottitehtävän seuraaviin vaiheisiin mennee pari, kolme tuntia. Robottitehtävän viimeisiin neljään vaiheeseen voi mennä neljä, viisikin tuntia lisää.

Pistearvo: A20 + B70 + C70.

Oheisprojektit: Robots. Pikkuesimerkkejä projektissa DoWhile (uusi).

../_images/person10.png

Johdanto

Luvuista 6.3 ja 6.4: Ohjelmointitarpeeseen voi löytyä korkean abstraktiotason työkalu, joka sopii juuri kyseisen ongelman ratkaisuun. Jos hyvää abstraktia työkalua ei ole, 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 myös 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.

Luvussa 7.1 jäsensimme tällaisia ongelmia ajattelemalla niitä syötteiden virtana, 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 = ""

Tee seuraavat asiat:
    name = readLine("Enter your name (at least one character, please): ")
    println("The name is " + name.length + (if (name.length != 1) " characters" else " character") + " long.")
    Tarkasta lopuksi, onko name-muuttujan arvona tyhjä merkkijono,
    ja mikäli näin on, niin tee nämä asiat uudestaan. Muuten jatka alla olevaan koodiin.

println("OK. Your name is " + name + ".")

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

do-silmukat

Tässä Scala-toteutus pseudokoodillemme. Se löytyy myös DoWhile-projektista.

var name = ""
do {
  name = readLine("Enter your name (at least one character, please): ")
  println("The name is " + name.length + (if (name.length != 1) " characters" else " character") + "long.")
} while (name.isEmpty)
println("OK. Your name is " + name + ".")
Määritellään silmukka käyttämällä alussa do-sanaa ja lopussa while-sanaa. Kyseessä ovat Scalan osiksi määritellyt avainsanat (kuten esim. if) eivätkä minkään olion metodit.
whilen perään kirjoitetaan kaarisuluissa ehtolauseke aivan kuin if-käskyssä. Tämän ehtolausekkeen arvo tarkastetaan aina kun yllä olevat rivit on suoritettu.
Silmukan sisälle kirjoitetut rivit suoritetaan ainakin kerran ja uudestaan aina, kun on todettu ehtolausekkeen arvoksi true. Tässä tapauksessa siis aina, kun nimi on tyhjä merkkijono.
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 tällaisen dowhile-silmukan eli lyhyemmin sanoen do-silmukan voi siis muotoilla näin:

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

do {
  Yksi tai useampia käskyjä, jotka suoritetaan ainakin kerran,
  ja joiden suorittamisen jälkeen tarkastetaan alla olevan ehdon paikkansapitävyys.
  Jos ehtolauseke on tosi, toistetaan käskyt.
} while (ehtolauseke)

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

Toinen do-esimerkki

Tutustu seuraavaan animaatioon ja tee sen yhteydessä pyydetty ennustus.

Mitä while-sana tarkoittaa?

Scalassa ja monessa muussa ohjelmointikielessä käytetty sana while eli "niin kauan kuin" johtaa joskus aloittelijaa harhaan. Esimerkiksi yllä olevan ohjelmakoodin kohdalla 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. Kuitenkin ehto 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.

Sana while esiintyy myös hieman toisenlaisen silmukkarakenteen määrittelyssä. Siitä lisää seuraavaksi.

while-silmukka

Vaihtoehto do-silmukalle on while-silmukka, jonka nimi tulee siitä, että sen määrittelyssä käytetään pelkkää while-avainsanaa.

Tässä do-silmukka ja while-silmukka allekkain. Kuten näkyy, ne ovat hyvin samankaltaiset:

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

do {
  Yksi tai useampia käskyjä, jotka suoritetaan ainakin kerran
  ja joiden suorittamisen jälkeen tarkastetaan alla olevan ehdon paikkansapitävyys.
  Jos ehtolauseke on tosi, toistetaan käskyt.
} while (ehtolauseke)

(Täältä jatketaan ohjelman suoritusta, kunhan ehto on tarkastettu ja todettu epätodeksi.)
do-silmukassa (yllä) while-sana ja jatkamisehto ovat silmukan ohjelmakoodin lopussa. while-silmukassa (alla) ne ovat alussa.
Vastaavasti do-silmukassa jatkamisehdon toteutuminen tarkastetaan aina sen jälkeen, kun silmukan sisältö on suoritettu. while-silmukassa taas aina ennen kuin silmukan sisältöä suoritetaan.
Edellisestä seuraa silmukoiden välinen ero: do-silmukan sisältö suoritetaan aina vähintään kerran, jonka jälkeen ehto tarkastetaan ensimmäistä kertaa. while-silmukan ehto taas tarkastetaan ensimmäistä kertaa heti silmukan aluksi, ja jos ensitarkastus tuottaa tuloksen false, ei sisältörivejä suoriteta kertaakaan.
(Ensin voi laittaa käskyjä, jotka suoritetaan kerran ennen silmukkaa.)

while (ehtolauseke) {
  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 käskyt.
}

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

Monissa tapauksissa do- ja while-silmukoilla on eroa hädin tuskin lainkaan. Esimerkiksi seuraava koodi tuottaa täsmälleen saman lopputuloksen kuin alkuperäinen do-silmukalla toteutettu versiokin.

var name = ""
while (name.isEmpty) { // nimi on aluksi tyhjä, joten suoritetaan ainakin kerran
  name = readLine("Enter your name (at least one character, please): ")
  println("The name is " + name.length + (if (name.length != 1) " characters" else " character") + " long.")
}
println("OK. Your name is " + name + ".")

do- ja while-silmukat muistuttavat toisiaan niin paljon, että minkä tahansa ohjelman, joka käyttää yhtä voi muuttaa varsin yksinkertaisesti käyttämään toista. (Vapaaehtoinen lisätehtävä: mieti miten.) Jos olemme päättäneet käyttää jompaakumpaa näistä silmukoista, voimme valita niiden välillä sillä perusteella, onko tarkoitus toistaa "yksi tai yli" kertaa (jolloin do-silmukka on luultavasti vähintään yhtä kätevä kuin while-silmukka) vai "nolla tai yli" kertaa (jolloin while-silmukka lienee kätevämpi).

Pikkutehtäviä: while ja do

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

var tulos = "TRO"
while (tulos.length < 10) {
  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!

Vertaa seuraavia koodinpätkiä:

def esimerkkiA(raja: Int) = {
  var luku = 1
  var nelio = 1
  do {
    println(nelio)
    luku += 1
    nelio = luku * luku
  } while (nelio <= raja)
}
def esimerkkiB(raja: Int) = {
  var luku = 1
  var nelio = 1
  while (nelio <= raja) {
    println(nelio)
    luku += 1
    nelio = luku * luku
  }
}

Millä kaikilla parametrimuuttujan arvoilla nämä funktiot tuottavat keskenään erilaisen tulosteen?

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) {
  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) {
  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 Eclipsessä punaisesta Terminate-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.1 teimme tällaisen pikkuohjelman:

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

Toteuta samoin toimiva ohjelma while- tai do-silmukalla virtojen sijaan. Kirjoita Task1.scalaan DoWhile-projektissa.

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.

do ja 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 = Stream.from(0).map( _ * 2  ).dropWhile( _ <= 20 ).head
val tulos = {
  var luku = 0
  var tupla = 0
  while (tupla <= 20) {
    luku += 1
    tupla = luku * 2
  }
  tupla
}
while-silmukan ehtoa vastaa virtaan perustuvassa toteutuksessa dropWhile-metodin parametrifunktio.

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

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

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

Stream.continually( Random.nextInt(100) ).map(tutki).find( _ == true )
var onIso = false
do {
  onIso = tutki(Random.nextInt(100))
} while (!onIso)
Tällä kertaa do-silmukan ehtoa vastaa find-metodikutsu.
Satunnaislukujen virtaa käydään läpi kutsuen tutki-metodia kullekin luvulle. Näin muodostetaan totuusarvojen virta.
Näitä totuusarvoja muodostetaan niin kauan, 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 virran muodostamaan alkioita trueen asti. Esimerkiksi contains(true) toimii myös.

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

Mitkä työkalut valitsisin?

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

Korkeamman asteen metodit sovellettuina virtoihin tai muihin kokoelmiin korostavat — niin koodissamme kuin ajatuksissamme — käsiteltävää dataa ja niitä toimenpiteitä, joita ohjelman halutaan tuohon dataan kohdistavan. Niitä käyttäessäsi jätät käskyjen suoritusjärjestyksen yksityiskohdat kirjastometodien huoleksi ja keskityt ilmaisemaan ohjelman tarkoituksen.

do- ja while-silmukat puolestaan korostavat peräjälkeistä suoritusjärjestystä. Niitä käyttäessäsi olet pikkuisen lähempänä sitä matalan abstrakiotason vaiheittaista toimintaa, joka tapahtuu tietokoneen prosessorissa, kun se ohjelmiasi käsittelee. Kun käytät näitä silmukoita, otat ohjelman suoritusjärjestyksen suoremmin haltuun ja määräät nimenomaisesti mikä käsky seuraa välittömästi mitäkin toista käskyä.

Monissa yhteyksissä do ja while ovat tarpeettoman yksityiskohtaisia ja kokoelmametodit kätevämpiä, luettavampia ja vähemmän alttiita virheille. Kuitenkin noiden ehdollisten silmukoiden käyttö voi olla perusteltua esimerkiksi jos:

  • Kyseessä on algoritmi, jonka on tarkoitus 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.

Tai jos:

  • On todettu tarpeelliseksi maksimoida kyseisen osaohjelman suoritustehokkuus ja halutaan yksityiskohtaisesti säädellä sitä, missä järjestyksessä sen suoritus etenee, minimoida ei-välttämättömät funktiokutsut tms.

Ehtolausekkeisiin perustuvat toistokäskyt kuten do ja while ovat tarjolla hyvin monissa ohjelmointikielissä, ja niitä 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 nämäkin työkalut, mutta älä suotta käytä niitä 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.1 käsittelimme virtoja, 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. Virtaa — toisin kuin silmukkaa — voi käsitellä kuin muutakin dataa: sille voi kutsua metodeita kuten map, jotka tuottavat toisensisältöisen virran, ja sellaisia kuten find, contains ja exists, jotka käyvät virran vain osin läpi. Siksi jotkut kutsuvat virtoja "ensiluokan silmukoiksi" (first-class loops).

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 ehtolausekkeisiin perustuvia while- ja do-silmukoita abstraktimpia. 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. Ratkaisut jäljellä oleviin vaiheisiin palautetaan kahdessa erässä, ensin vaiheet 5 ja 6 (joista saa B-pisteitä) ja sitten vaiheet 7 ja 8 (joista saa C-pisteitä).

Robottitehtävä, vaihe 5/10: liikkumisen välineet

RobotBrain-luokasta puuttuu robotin liikkumiseen liittyviä metodeita. Toteuta metodit locationInFront, squareInFront, robotInFront ja moveCarefully.

Robottitehtävä, vaihe 6/10: Nosebot

  1. Toteuta liikkuva robottityyppi: luokka Nosebot. Luokasta puuttuu kaksi metodia: bottia liikuttava moveBody ja sen avuksi sopiva attemptMove. Aloita viimeksi mainitusta ja käytä sitä moveBodyn toteutuksessa.

Virta vai silmukat?

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

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

  1. Kokeile nenäbotteja. Huomaa, että kun kokonaisuus on laadittu hyvin, on helppoa lisätä uusi robottityyppi simulaattoriin. Oikeastaan ainoa, mikä piti kirjoittaa, on toteutus algoritmille, jolla nenäbotit valitsevat, mihin liikkuvat.
  2. Palauta ratkaisusi vaiheisiin 5 ja 6 tällä lomakkeella.

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

Robottitehtävä, vaihe 7/10: törmäykset

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

  1. Huomaa ensinnäkin: Wall ja Floor on määritelty samassa tiedostossa Squaren kanssa: Square.scala. Näin on päätetty tehdä, koska koodia on vähän ja nämä tietotyypit liittyvät toisiinsa hyvin läheisesti.
  2. Wall-yksittäisolion addRobot-metodi ei tee nyt mitään paitsi ilmoittaa palautusarvollaan, ettei robotti mahtunut seinän kanssa samaan ruutuun. Täydennä sitä niin, että se rikkoo ruutuun yrittävän robotin kuten dokumentaatio määrää.
  3. 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 8/10: 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, eikä testiohjelma myönnä pisteitä.)

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

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

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

Robottitehtävä, vaihe 9/10: Lovebot

Toteuta Lovebot.

yDirectionOf-metodista

Saatat käyttää ratkaisussasi GridPos-luokan yDirectionOf-metodia (joka on kuvattu dokumentaatiossa). Jos käytät, niin korjaa ensin O1Libraryn tämänvuotiseen versioon lipsahtanut bugi, joka sinunkin kopiossasi on, mikäli aloitit kurssin aikataulussa syyskuussa 2019 ja otit O1Libraryn käyttöön silloin.

  1. Avaa O1Library-projekti Eclipsessä, etsi sieltä pakkaus o1.grid ja tiedosto GridPos.scala.
  2. Etsi tiedoston lopusta metodista yDirectionOf kohta, jossa lukee this.xDiff(another), kun pitäisi lukea this.yDiff(another).
  3. Korjaa ja tallenna.

Niin, ja tuo metodi siis voi olla tässä tehtävässä ihan kätevä. Mitenkään välttämätön se ei ole.

Uskot tietenkin, että ihan tarkoituksella järjestimme teille tällaisen tosielämän debuggaustilaisuuden.

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

Robottitehtävä, vaihe 10/10: Psychobot

Toteuta Psychobot.

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

GridPos-luokan metodeista voi olla apua; muista dokumentaatio.

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

Yhteenvetoa

  • do- ja while-silmukat ovat yleisiä monissa ohjelmointikielissä ja ohjelmissa. Niillä voi toistaa käskyä tai käskyjä niin kauan kuin tietty ehtolauseke on tosi.
    • Ehtolausekkeen totuusarvo tarkastetaan aina kunkin suorituskerran alussa (while) tai lopussa (do).
    • 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 do- ja while-silmukat.
  • Lukuun liittyviä termejä sanastosivulla: silmukka, do-silmukka, while-silmukka, iteraatio; virta; abstraktiotaso.

break- ja continue-käskyt? Entä return? Erilainen for-silmukka?

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 kuitenkin 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 kovin usein käytetä. On melko epätavallista, ettei parempaa ratkaisutapaa löytyisi kuin tuon käskyn käyttö, joskin kyseessä on enimmäkseen 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:

def ekaJokaEiOleTyhja(vektori: Vector[String]): Option[String] = {
  for (alkio <- vektori) {
    if (alkio.length > 0) {
      return Some(alkio)  // Löytyi: lopetetaan heti käymättä läpi muita alkioita.
    }
  }
  return None
}
return-käsky katkaisee silmukan ja koko funktion suorituksen ja palauttaa return-sanan perässä olevan 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 palautusarvon 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 voi tehdä ohjelman suoritusvaiheiden (control flow) seuraamisesta vaikeampaa. 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, yhteen toimiviin 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 kuten find sekä rekursio (luku 12.1). Korkeamman asteen funktiot ja rekursio ovat yleisiä erityisesti funktionaalisessa ohjelmoinnissa (luku 10.2), mutta eivät suinkaan ole sidottuja vain siihen.

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

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 koodissa olevaa 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 kaukana ylempänä oleva return-käsky 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ää silmukan 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ä?

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.

Palaute

../_images/be_back.png

Robotit tekevät paluun luvussa 11.2.

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

Tekijät

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

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

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

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

Tehtävien automaattisen arvioinnin ovat toteuttaneet: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä ja Aleksi Vartiainen.

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

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

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

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

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

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

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

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

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