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).
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 + ".")
while
n perään kirjoitetaan kaarisuluissa ehtolauseke aivan
kuin if
-käskyssä. Tämän ehtolausekkeen arvo tarkastetaan aina
kun yllä olevat rivit on suoritettu.true
.
Tässä tapauksessa siis aina, kun nimi on tyhjä merkkijono.while
-rivillä todettu ehdon olevan
false
ja sillä perusteella päätetty lopettaa silmukan
suorittaminen.Yleisemmin tällaisen do
–while
-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.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.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
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
}
var
-muuttujalle asetetaan alkuarvo.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.
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.scala
an DoWhile-projektissa.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Toinen samantapainen harjoitus
Toteuta Task2.scala
an 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)
do
-silmukan ehtoa vastaa find
-metodikutsu.tutki
-metodia
kullekin luvulle. Näin muodostetaan totuusarvojen virta.true
. Tällöin find
ei enää jatka myöhempien
alkioiden tutkimista eikä niitä edes muodosteta.find
in sijaan olisimme voineet käyttää muutakin metodia, joka pakottaa virran
muodostamaan alkioita true
en asti. Esimerkiksi contains(true)
toimii myös.
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
- Toteuta liikkuva robottityyppi: luokka
Nosebot
. Luokasta puuttuu kaksi metodia: bottia liikuttavamoveBody
ja sen avuksi sopivaattemptMove
. Aloita viimeksi mainitusta ja käytä sitämoveBody
n toteutuksessa.
Virta vai silmukat?
moveBody
n 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ää
map
iä jafind
iä hieman samaan tapaan kuin yllä olevassa esimerkkikoodissa.
- 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.
- 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
Spinbot
it ja Nosebot
it 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ä.
- Huomaa ensinnäkin:
Wall
jaFloor
on määritelty samassa tiedostossaSquare
n 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. Wall
-yksittäisolionaddRobot
-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ää.- Täydennä myös
Floor
-olioidenaddRobot
-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.
- Avaa O1Library-projekti Eclipsessä, etsi
sieltä pakkaus
o1.grid
ja tiedostoGridPos.scala
. - Etsi tiedoston lopusta metodista
yDirectionOf
kohta, jossa lukeethis.xDiff(another)
, kun pitäisi lukeathis.yDiff(another)
. - 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
- jawhile
-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.
- Ehtolausekkeen totuusarvo tarkastetaan
aina kunkin suorituskerran alussa (
for
-silmukat ja korkeamman asteen metodit ovat usein riittävän tehokkaita ja huomattavasti kätevämpiä kuindo
- jawhile
-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?
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.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 return
ia, 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;
return
iin 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 return
ille 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 return
in 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ä
break
-käskyä karsastavat vielä useammat kuin return
ia.
break
ia on kritisoitu pitkälti samanlaisin perusteluin kuin
return
iakin. 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
return
illa break
in sijaan. (Sen jälkeen voit miettiä,
voisitko toteuttaa senkin metodin jollakin muulla kuin
return
illa.)
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
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.
do
-sanaa ja lopussawhile
-sanaa. Kyseessä ovat Scalan osiksi määritellyt avainsanat (kuten esim.if
) eivätkä minkään olion metodit.