Kurssin viimeisimmän version löydät täältä: O1: 2024
Luku 9.1: 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ökalu:
while
-silmukat. Robottisimulaattorin luokkahierarkiaa. Myös
LazyList
ien käytöstä opitaan toivottavasti lisää.
Mitä tehdään? Ensin luetaan, sitten ohjelmoidaan.
Suuntaa antava työläysarvio:? Yksi kurssin työläimmistä luvuista. Luvun teksteihin ja robottitehtävän seuraavaan pariin vaiheeseen mennee pari, kolme tuntia. Robottitehtävän viimeisiin vaiheeseen voi mennä neljä, viisikin tuntia lisää.
Pistearvo: A15 + B50 + C85.
Oheismoduulit: Robots. Pikkuesimerkkejä moduulissa WhileLoops (uusi).
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!")
while
n ja do
n 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 laittaa 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 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.
Koodinlukutehtäviä: while
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.
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.scala
an WhileLoops-moduulissa.
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.
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 LazyList
iin 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.
find
in sijaan olisimme voineet käyttää muutakin metodia, joka pakottaa laiskalistan
muodostamaan alkioita true
en asti. Esimerkiksi contains(true)
toimii myös.
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 hyvin 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
:
Täydennä luokan otsikkorivi parametreineen kuntoon.
Toteuta yksinkertainen
mayAdvance
. (Muistaoverride
.)Lisäksi puuttuu kaksi liikkumismetodia:
moveBody
ja sen avuksi sopivaattemptMove
. Aloita viimeksi mainitusta ja käytä sitämoveBody
n toteutuksessa. HuomaaattemptMove
-metodin palautusarvo, jota voi hyödyntää.LazyList
vai silmukat?moveBody
n 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äämap
iä jafind
iä hieman samaan tapaan kuin yllä olevassa esimerkkikoodissa.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 palautuutrue
. Palautusarvon vuoksi kutsutaan samaa funktiota uudestaan ja tulostuu taas "moi".Vastaavasti on saattanut käydä sinullekin, kun olet käyttänyt
attemptMove
-metodia.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.
Palauta ratkaisusi ennen kuin etenet seuraavaan vaiheeseen.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Robottitehtävä, vaihe 6/9: 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, törmäilevät. Tässä vaiheessa
pohjustat viimeistä vaihetta täydentämällä Square
n alatyyppejä.
Wall
ja Floor
on määritelty samassa tiedostossa Square
n kanssa: Square.scala
.
Täydennä ne:
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 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, eikä testiohjelma 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: Psychobot
Toteuta Psychobot
.
Tässä on jälleen mahdollisuus käyttää silmukkaa ja/tai LazyList
in 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 käskyä tai käskyjä 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ä kuinwhile
-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?
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 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.
(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 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 vaikeuttaa joskus ohjelman suoritusvaiheiden
(control flow) seuraamista. 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 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 (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 return
in käyttö Scalassa silti kuolemansynti ole, varsinkaan
jos pidät koodisi muuten siistinä.
(Jyrkempiäkin mielipiteitä on <https://tpolecat.github.io/2014/05/09/return.html>
,
ja niillekin löytyy järkiperusteita. Mitä funktionaalisemmalla
tyylillä ohjelmoidaan, sitä perustellumpaa on välttää return
ia.)
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ä
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ää 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ä — 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
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, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, 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 ja Juha Sorva. 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. Pääkehittäjänä on nyt Markku Riekkinen, jonka lisäksi A+:aa 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.
Määritellään silmukka sanalla
while
. Apuna on myösfor
-silmukoista jo tuttu sanado
.