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

Luku 5.5: Silmukoita

Tästä sivusta:

Pääkysymyksiä: Miten käsittelen paljon dataa kerralla? Miten toistan jonkin toimenpiteen kaikille kokoelman sisältämille alkioille?

Mitä käsitellään? Kokoelman läpikäynti for-silmukalla. Algoritmien luominen käyttämällä paikallisia muuttujia silmukan kanssa.

Mitä tehdään? Luetaan ja tehdään jokunen pikkutehtävä. Lisää tehtäviä samasta aiheesta löytyy seuraavassa luvussa.

Suuntaa antava työläysarvio:? Reilu tunti.

Pistearvo: A30.

Oheismoduulit: AuctionHouse1, ForLoops (uusi).

../_images/person03.png

Tausta: muuttuvahintaisia esineitä

Luvussa 5.1 toteutit toivottavasti ainakin FixedPriceSale-luokan, joka kuvasi vakiohintaan kaupattavia esineitä. Luvussa oli myös vapaaehtoiset tehtävät, joissa toteutettiin luokat EnglishAuction ja DutchAuction. Käytämme näistä kohta EnglishAuctionia.

Jos et tehnyt tuota tehtävää, niin se ei tässä haittaa. Kuten FixedPriceSale-olio myös EnglishAuction-olio kuvaa kaupattavaa esinettä. Tällaisella huutokaupalla on hinta (price), mahdollinen ostaja ( buyer) ja advanceOneDay-metodi aivan vastaavasti kuin FixedPriceSale-oliolla. Erona on, että EnglishAuction-oliossa hinta nousee eri ostajaehdokkaiden tarjoutuessa maksamaan esineestä suurempia summia. Siinä kaikki, mitä sinun on välttämätöntä nyt tietää asiasta.

Silti halutessasi voit...

... käydä tässä välissä tekemässä tuon tehtävän luvusta 5.1 tai katsoa sen ratkaisun tästä:

EnglishAuctionin toteutus

Näytä koodiPiilota koodi

class EnglishAuction(val description: String, val startingPrice: Int, duration: Int):

  private var highest = Bid(None, startingPrice)
  private var secondHighest = Bid(None, startingPrice)
  private var remainingDays = duration

  def daysLeft = this.remainingDays

  def advanceOneDay() =
    if this.isOpen then
      this.remainingDays -= 1

  def isOpen = this.remainingDays > 0

  def hasNoBids = this.highest.isInitialBid

  def isExpired = !this.isOpen && this.hasNoBids

  def buyer = this.highest.bidder

  def price =
    if this.secondHighest.isInitialBid then
      this.startingPrice
    else
      min(this.secondHighest.limit + 1, this.highest.limit)

  def requiredBid = if this.hasNoBids then this.startingPrice else this.price + 1

  def bid(bidder: String, amount: Int) =
    val newBid = Bid(Some(bidder), amount)
    if this.isOpen && amount >= this.requiredBid then
      this.secondHighest = if newBid.beats(this.highest) then this.highest
                           else newBid.winner(this.secondHighest)
      this.highest = newBid.winner(this.highest)
    end if
    this.highest == newBid

  override def toString = this.description

end EnglishAuction

Oleellista tämän luvun kannalta on vain se, että esineen hinta ei ole vakio vaan määrittyy tehtyjen huutojen perusteella.

Huutoja tehdään bid-metodilla, joka siis vaikuttaa esineen nykyhintaan.

Apuluokka Bid löytyy AuctionHouse1-moduulista. EnglishAuction käyttää sitä sisäisesti, eikä tuo luokka ole tärkeä.

Luvun tavoite: AuctionHouse-luokka

Oletetaan siis, että käytössämme on EnglishAuction-luokka. Otetaan nyt tavoitteeksi laatia lisäksi luokka AuctionHouse: yksi AuctionHouse-olio edustaa huutokauppapaikkaa, joka sisältää EnglishAuction-olioita eli hinnaltaan muuttuvia esineitä.

Tässä alkua AuctionHouse-luokalle:

class AuctionHouse(val name: String):

  private val items = Buffer[EnglishAuction]()

  def addItem(item: EnglishAuction) =
    this.items += item

  def removeItem(item: EnglishAuction) =
    this.items -= item

  override def toString = this.name

  // Muita metodeita tulee tänne.

end AuctionHouse

Kullakin AuctionHouse-oliolla on oma puskuri, johon säiliömuuttuja items viittaa. Siihen voi lisätä huutokauppoja, ja niitä voi poistaakin.

Nyt haluttaisiin luoda AuctionHouse-luokkaan metodeita, joilla voi

  • edistää jokaista huutokauppaa päivällä (eli kutsua jokaisen items-puskurissa olevan EnglishAuction-olion advanceOneDay-metodia),

  • laskea kaikkien huutokauppojen yhteishinnan,

  • laskea huutokauppojen keskimääräisen hinnan,

  • etsiä huutokaupoista kaikkein hintavimman,

  • laskea avoinna olevien huutokauppojen lukumäärän, ja

  • tuottaa luettelon kaikista tietyn ostajan voittamista huutokaupoista.

Alla on esimerkkejä siitä, miten näiden metodien tulisi toimia.

Käyttöesimerkki

Uusi kauppapaikka olisi tarkoitus voida luoda näin:

val house = AuctionHouse("ReBay")house: AuctionHouse = ReBay

Yhtään esinettä ei vielä ole myynnissä. Siksi esimerkiksi priciest-metodi, jonka tehtävä on palauttaa kallein esine, tuottaa palautusarvoksi None:

house.priciestres0: Option[EnglishAuction] = None

Pistetään kolme huutokauppaa pystyyn ja lisätään ne addItem-metodilla AuctionHouseen. Kullakin esineellä on kuvaus, alkuhinta ja kesto päivissä.

val bag = EnglishAuction("A glorious handbag", 100, 14)bag: EnglishAuction = A glorious handbag
house.addItem(bag)val camera = EnglishAuction("Nikon COOLPIX", 150, 3)camera: EnglishAuction = Nikon COOLPIX
house.addItem(camera)house.addItem(EnglishAuction("Collectible Easter Bunny China Thimble", 1, 10))

Seuraava tuloste osoittaa, että nextDay-metodikutsut vähentävät sekä laukun että kameran (ja siis kaikkien käynnissä olevien huutokauppojen) kestoaikaa päivällä:

println(bag.daysLeft + ", " + camera.daysLeft)14, 3
house.nextDay()
house.nextDay()
println(bag.daysLeft + ", " + camera.daysLeft)12, 1

totalPrice ja averagePrice palauttavat tilastotietoja:

house.totalPriceres1: Int = 251
house.averagePriceres2: Double = 83.66666666666667

Kun kaupattavista esineistä tarjotaan suurempia summia, niiden hinta nousee, mikä näkyy mainituissa tilastoissa.

bag.bid("Minna", 200)res3: Boolean = true
bag.bid("Mikko", 170)res4: Boolean = false
camera.bid("Minna", 190)res5: Boolean = true
house.totalPriceres6: Int = 322

Huutoja tehdään bid-metodilla. Kilpailevat huudot voivat nostaa EnglishAuction-olion hintaa. (Tarkemmat yksityiskohdat eivät ole tässä merkityksellisiä mutta on kuvattu luokan EnglishAuction dokumentaatiossa. Nyt oleellista on, että bid voi muuttaa esineen hintaa.)

AuctionHouse-olion pitäisi osata laskea ja raportoida huutojen vuoksi päivittynyt kokonaishinta

Hinnankorotusten jälkeen hintavin esine on laukku:

house.priciestres7: Option[EnglishAuction] = Some(A glorious handbag)

Tässä vaiheessa kaikki kolme esimerkkihuutokauppaamme ovat vielä auki, mutta kameralla on vain yksi myyntipäivä jäljellä. Kauppa sulkeutuu seuraavasta nextDay-kutsusta. Tämän havaitsee vaikkapa kysymällä AuctionHouse-oliolta, montako avointa kauppaa on jäljellä:

house.numberOfOpenItemsres8: Int = 3
house.nextDay()house.numberOfOpenItemsres9: Int = 2

Katsotaan vielä, mitä kukakin on saamassa:

house.purchasesOf("Minna")res10: Vector[EnglishAuction] = Vector(A glorious handbag, Nikon COOLPIX)
house.purchasesOf("Mikko")res11: Vector[EnglishAuction] = Vector()

purchasesOf-metodin palauttamissa vektoreissa ovat sekä sellaiset päättyneet huutokaupat, jotka kyseinen ostaja on jo voittanut (kuten tässä kamera, joka sulkeutui Minnan tehtyä korkeimman tarjouksen), että sellaiset vielä avoimet huutokaupat, joissa hän on johtoasemassa (kuten tässä Minnan laukku).

Mitä tarvitaan?

Mitä yhteistä toivotuilla metodeilla on?

  1. Kunkin niistä toteuttamiseksi on osattava käydä läpi AuctionHouse-olioon liitetyt huutokaupat eli items-puskurin alkiot.

  2. Kuhunkin niistä sisältyy ajatus siitä, että yhtä tai useampaa käskyä toistetaan useita kertoja.

Alkiokokoelman läpikäynti ja toimenpiteen suorittaminen kunkin kokoelman sisältämän alkion kohdalla on ohjelmissa erittäin yleinen tarve. Melkein kaikki vähänkään kiinnostavat tietokoneohjelmat toistavat käskyjä tavalla tai toisella.

Tässä luvussa opit käyttämään Scala-käskyä, jolla homma onnistuu. Tarkastellaan metodi kerrallaan, miten AuctionHouse saadaan toteuttua halutunlaiseksi.

nextDay-metodi

Suunnitellaan algoritmi nextDay-metodille:

def nextDay() =
  Vuorotellen jokaiselle alkiolle tämän olion huutokauppaluettelossa tee näin:
    - Kutsu kyseisen huutokaupan advanceOneDay-metodia.

Tarkennetaan:

def nextDay() =
  Vuorotellen jokaiselle alkiolle tämän olion huutokauppaluettelossa tee näin:
    - Ota tuorein alkio (viittaus viimeisimpään läpikäytyyn huutokauppaan)
       talteen paikalliseen muuttujaan.
    - Kutsu talteen otetulle huutokaupalle advanceOneDay-metodia.

Tarkennetaan:

def nextDay() =
  Vuorotellen jokaiselle alkiolle puskurissa this.items tee näin:
    - Ota alkio talteen tuoreimman säilyttäjään current.
    - Kutsu current.advanceOneDay().

Metodin olisi siis toimittava kuten seuraava animaatio näyttää.

Vaikka tämä olikin vasta pseudokoodia, niin animaatio korosti, miten samat koodin osat tulevat suoritetuiksi useita kertoja. Kuitenkin joka "kierroksella" luodaan aina uusi current-muuttuja uudella arvolla. Metodiin muodostuu niin sanottu silmukka (eli "luuppi"; loop), joka toistaa tiettyä toimenpidettä.

for-silmukat

Pseudokoodista Scalaksi

Alla on eräs kätevä tapa toteuttaa nextDay-luonnoksemme Scalalla. Tämä koodi toimii juuri niin kuin äskeisessä animaatiossa näytettiin.

def nextDay() =
  for current <- this.items do
    current.advanceOneDay()
   

Tällaisen silmukan avainsanat ovat for ja do.

Alun voi lukea "for each item current in this.items, do" tai "vuorotellen jokaiselle alkiolle current, joka löytyy kokoelmasta this.items, tee seuraavaa".

Tässä tulee määritellyksi muuttuja, jolle voimme valita nimen; olemme nyt valinneet nimeksi current. Tämä nimi viittaa jokaisella silmukan "kierroksella" eri alkioon. Alkiot poimitaan kokoelmasta, joka...

... on mainittu vasemmalle osoittavan nuolen <- perässä.

Silmukan runko tulee toistetuksi kerran jokaiselle this.items-kokoelman alkiolle. Runko on sisennetty pykälää syvemmälle.

Voit kirjoittaa silmukalle loppumerkin. Usein ne jätetään kirjoittamatta, mutta jos kyse on hieman mutkikkaammasta metodista, saattaa loppumerkki selkiyttää koodia. Saat kirjoittaa vaikka joka silmukan loppuun end for, jos siltä tuntuu.

for-silmukan rakenne

Tämä pseudokoodi kuvaa yleisemmin, miten alkiokokoelmaa voi käydä läpi ja sen kutakin alkiota käsitellä for-silmukalla:

for muuttuja <- alkiokokoelma do
  Tee jotain alkiolla, joka on varastoitu muuttujaan.
end for    // Tämän viimeisen rivin voi poistaakin.

Silmukan määrittelyyn sisältyy muuttujan määrittely. Tähän muuttujaan tallentuu aina viimeisin läpikäydyistä alkioista, ja se on siis rooliltaan tuoreimman säilyttäjä.

Läpikäytävä alkiokokoelma voi olla esimerkiksi puskuri (kuten tässä ensimmäisessä esimerkissä), vektori (luku 4.2), merkkijono (luku 5.6), taulukko (luku 12.1), lista (luku 7.2) tai hakurakenne (luku 9.2).

Kun for-silmukka käy läpi puskurin tai vektorin kaltaista kokoelmaa, jossa alkioilla on indeksit, alkiot tulevat käsitellyksi indeksien mukaisessa järjestyksessä.

Rooleista

Luvussa 2.6 totesimme, että muuttujia voi luokitella niiden roolin mukaan:

../_images/variables_role-fi.png

Luku 2.6 näytti esimerkkejä muuttujista, joiden rooliksi mainittiin tuoreimman säilyttäjä. Tällainen oli esimerkiksi työntekijäluokan ilmentymämuuttuja nimi, jolle saattoi sijoittaa uuden arvon korvaamaan vanhan; vain viimeisin asetettu nimi jäi voimaan:

class Tyontekija(var nimi: String, val syntynyt: Int, var kkpalkka: Double):
  // ...
end Tyontekija

Tässä luvussa taas olemme nähneet aivan toisenlaisessa tilanteessa paikallisen current-muuttujan, jota sitäkin sanottiin tuoreimman säilyttäjäksi:

for current <- this.items do
  current.advanceOneDay()

Ulkoisesti näiden muuttujien käyttötapa näyttää hyvin erilaiselta. Kuitenkin molemmissa tapauksissa ajatuksena on, että muuttujassa säilytetään viimeisintä arvosekvenssissä kohdattua arvoa: viimeisin asetettu nimi, viimeisin kohdattu huutokauppaolio. Koodissa näkyvä ero kumpuaa siitä, että yksi on ilmentymämuuttuja ja toinen paikallinen muuttuja. Molemmat ovat tyyppiesimerkkejä tilanteista, joissa tuoreimman säilyttäjästä on hyötyä:

  • "Asetettavissa oleva ominaisuus" kuten nimi on tyypillinen tuoreimman säilyttäjä -ilmentymämuuttujan käyttötapa. Se esiintyy tilanteissa, joissa tuoreinta arvoa on tarpeen säilyttää ilmentymämuuttujassa myös metodikutsujen välillä.

  • "Viimeisin silmukassa läpikäytävistä arvoista" kuten current on tyypillinen paikallisen tuoreimman säilyttäjä -muuttujan käyttötapa. Se esiintyy tilanteissa, joissa tuoreinta arvoa tarvitaan vain tietyn metodin toteutuksessa sisäisesti apuna.

Monelle tutulle muuttujaroolille löytyy uusia käyttötapoja, kun tuo rooli annetaan paikalliselle muuttujalle, jota käytetään yhdessä silmukan kanssa. Tämä käy ilmi, kun seuraavaksi toteutamme AuctionHouse-luokan muita metodeita.

Paikallinen kokooja + silmukka

totalPrice-metodi

Pseudokoodiluonnos totalPrice-metodista:

def totalPrice =
  Vuorotellen jokaiselle huutokaupalle puskurissa this.items tee näin:
    - Selvitä huutokaupan hinta ja lisää summaan.
  Lopuksi palauta summa.

Millaisia paikallisia muuttujia tarvitaan?

  • Tuoreimman säilyttäjä (tyyppiä EnglishAuction) pitämään kirjaa siitä alkiosta (huutokaupasta), jossa ollaan menossa. Siis aivan kuin nextDay-esimerkissä.

  • Kokooja (tyyppiä Int), johon kootaan vaihe vaiheelta huutokauppojen hintojen summa.

Tarkennetaan.

def totalPrice =
  Olkoon totalSoFar kokooja, jonka arvo on aluksi nolla.
  Vuorotellen jokaiselle huutokaupalle puskurissa this.items tee näin:
    - Selvitä huutokaupan hinta ja korota totalSoFarin arvoa sen verran.
  Palauta lopuksi kokoojan totalSoFar arvo.

Sama Scalalla:

def totalPrice =
  var totalSoFar = 0
  for current <- this.items do
    totalSoFar += current.price
  totalSoFar

Kyseenalaistus

Oliko tuo nyt tarpeen? Ei olisi tarvittu silmukkaakaan, kun olisi tehty näin:

class AuctionHouse(val name: String):

  private val items = Buffer[EnglishAuction]()
  private var priceSoFar = 0

  def addItem(item: EnglishAuction) =
    this.items += item
    this.priceSoFar += item.price

  def removeItem(item: EnglishAuction) =
    this.items -= item
    this.priceSoFar -= item.price

  def totalPrice = this.priceSoFar

end AuctionHouse

Tässä ratkaisussa kokoojamuuttuja onkin laitettu ilmentymämuuttujaksi: kunkin AuctionHouse-olion ominaisuudeksi kirjataan tieto sen sisältämien huutokauppojen yhteishinnasta.

Huutokauppoja lisättäessä ja poistaessa tätä ilmentymämuuttujaa päivitetään.

totalPrice-metodi on nyt todella yksinkertainen.

Toimisiko äskeinenkin toteutus? Vastaa kysymykseen itse miettimällä seuraavaa koodia.

val kamari = AuctionHouse("Huutelu.net")
val esine = EnglishAuction("telkkari", 5000, 10)
kamari.addItem(esine)
println(kamari.totalPrice)
esine.bid("Matti", 10000)
esine.bid("Teppo", 15000)
println(kamari.totalPrice)

Jos et keksi, mikä on ongelmana, kommentoi asiaa luvun lopussa palautelomakkeella, niin aihetta voidaan vaikkapa käsitellä viikkokoosteessa.

averagePrice-metodi

Keskiarvometodin saa tehtyä aivan helposti, kun luokassa on myös summanlaskemismetodi:

def averagePrice = this.totalPrice.toDouble / this.items.size

Paikallinen askeltaja + silmukka (feat. if)

Pseudokoodi numberOfOpenItems-metodille:

def numberOfOpenItems =
  Vuorotellen jokaiselle huutokaupalle puskurissa this.items tee näin:
    - Jos huutokauppa on auki, kasvata laskuria yhdellä.
  Lopuksi palauta laskurin arvo.

Tarkennetaan käyttäen muuttujaa askeltajana:

def numberOfOpenItems =
  Olkoon openCount askeltaja, jonka arvo on 0 ja joka kasvaa myöhemmin yksi kerrallaan.
  Vuorotellen jokaiselle huutokaupalle puskurissa this.items tee näin:
    - Ota huutokauppa talteen tuoreimman säilyttäjään current.
    - Jos huutokaupan isOpen palauttaa true, niin nosta openCountin arvoa yhdellä.
  Lopuksi palauta openCountin arvo.

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

Paikallinen säiliö + silmukka

purchasesOf-metodin kuului palauttaa vektori, jossa ovat tietyn asiakkaan ostokset. Ostoksiksi lasketaan ne esineet, jotka tuo asiakas on jo ostanut, ja ne, joissa hän on johtava huutaja.

Luonnos:

def purchasesOf(buyer: String) =
  Luo uusi tyhjä puskuri, johon viittaa säiliömuuttuja nimeltä purchases.
  Vuorotellen jokaiselle huutokaupalle puskurissa this.items tee näin:
    - Jos esineen ostajaksi on kirjattu parametrin buyer arvo,
      niin lisää esine purchases-säiliöön.
  Lopuksi palauta vektori, jossa on purchases-muuttujan mukaiset esineet.

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

Miksi vain EnglishAuctioneita?

Mitä jos halutaan AuctionHouseen myös FixedPriceSaleja ja DutchAuctioneita (luku 5.1)? Niitähän ei saa laitettua tuohon Buffer[EnglishAuction]-tyyppiseen puskuriin, jollaisia items ja oheinen purchases ovat?

Houkuttaisi tehdä items-puskuri, jossa olisi eri tavoin myyntiin laitettuja esineitä sekaisin, ja nämä tämän luvun metodit sitten käsittelisivät näitä kaikkia kerralla. Onnistuuko?

Onnistuu. Tähän opitaan välineitä luvussa 7.3, ja AuctionHouse-moduuli syntyy uudelleen entistä ehompana.

Paikallinen sopivimman säilyttäjä + silmukka

priciest ensin pseudokoodina:

def priciest =
  Olkoon hintavin tähän mennessä löydetty huutokauppa aluksi puskurin this.items ensimmäinen alkio.
  Vuorotellen jokaiselle huutokaupalle puskurissa this.items tee näin:
    - Vertaa huutokauppaa hintavimpaan aiemmin löydettyyn ja pistä muistiin näistä isompihintainen.
  Lopuksi palauta huutokauppa, joka jäi muistiin isohintaisimpana.

Millaisia paikallisia muuttujia tarvitaan?

  • Tuoreimman säilyttäjä (tyyppiä EnglishAuction) pitämään kirjaa siitä alkiosta (huutokaupasta), jossa ollaan menossa.

  • Sopivimman säilyttäjä (myös tyyppiä EnglishAuction), jossa pidetään kirjaa isohintaisimmasta toistaiseksi löydetystä huutokaupasta.

Pseudokoodiamme voi tarkentaa esimerkiksi näin:

def priciest =
  Olkoon priciestSoFar sopivimman säilyttäjä, jonka alkuarvo on this.items.head.
  Vuorotellen jokaiselle huutokaupalle puskurissa this.items tee näin:
    1. Ota huutokauppa talteen tuoreimman säilyttäjään current.
    2. Tutki, onko current isompihintainen kuin priciestSoFar. Jos on, niin:
        - Sijoita priciestSoFarin uudeksi arvoksi current.
  Lopuksi palauta sopivimman säilyttäjään priciestSoFar jäänyt arvo.

Ja sama Scalalla:

def priciest =
  var priciestSoFar = this.items.head
  for current <- this.items do
    if current.price > priciestSoFar.price then
      priciestSoFar = current
  priciestSoFar

Mitä unohtui?

Metodin oli määrätty toimivan niin, että se palauttaa Option[EnglishAuction]-tyyppisen arvon eikä aiheuta virhettä silloinkaan, kun huutokauppoja ei ole kauppakamarissa ensimmäistäkään. Tällä hetkellä palautusarvon tyyppi on kuitenkin EnglishAuction, ja jos items on tyhjä, metodin kutsuminen aiheuttaa ajonaikaisen poikkeustilanteen (koska tällöin edes indeksillä 0 ei ole alkiota).

Ongelma on samankaltainen kuin se, joka tuli vastaan luvussa 4.2, kun ensimmäisen kerran käytimme sopivimman säilyttäjää. Silloin unohdimme käsitellä addExperience-metodissa tapauksen, jossa aiempaa suosikkikokemusta ei ollutkaan.

Korjataan. Yksi korjaustapa on käsitellä tyhjän puskurin tapaus erikseen if-käskyllä.

def priciest =
  if this.items.isEmpty then
    None
  else
    var priciestSoFar = this.items.head
    for current <- this.items do
      if current.price > priciestSoFar.price then
        priciestSoFar = current
    Some(priciestSoFar)

Palautetaan None, jos läpikäytävää sisältöä ei ole.

Muussa tapauksessa puskurin ensimmäisen alkion voi huoletta poimia, koska alkioita on ainakin yksi...

... ja lopputulos kääritään Some-olioon.

Jäikö häiritsemään?

Tässä toteutuksessa priciestSoFar ja current viittaavat silmukan ensimmäisellä kierroksella samaan huutokauppaolioon. Tällöinhän niiden vertaileminen on ihan turhaa. Käytännössä tästä ei ole merkittävää haittaa, mutta hieman epäsiistiltä se silti tuntuu.

Voit pitää asian mielessä kurssin edetessä. Tämäkin metodi on toteutettavissa erilaisilla tavoilla, joista kaikissa ei tuota tarpeetonta itseensävertailua tehdä.

Entä se vanha Category-luokka metodeineen?

Luvussa 4.3 laadittiin erilaisia versioita GoodStuff-ohjelman Category-luokasta. Muun muassa tämä:

class Category(val name: String, val unit: String):

  private val experiences = Buffer[Experience]()
  private var fave: Option[Experience] = None

  def favorite = this.fave

  def addExperience(newExperience: Experience) =
    this.experiences += newExperience
    this.fave match
      case None =>
        this.fave = Some(newExperience)
      case Some(oldFave) =>
        val newFave = newExperience.chooseBetter(oldFave)
        this.fave = Some(newFave)

end Category

Tämänhän voisi tehdä myös samaan tapaan kuin priciest yllä, eikö totta? Tehdään tuoreimman säilyttäjästä fave ilmentymämuuttujan sijaan paikallinen muuttuja ja käytetään for-silmukkaa:

class Category(val name: String, val unit: String):

  private val experiences = Buffer[Experience]()

  def favorite =
    if this.experiences.isEmpty then
      None
    else
      var fave = this.experiences(0)
      for current <- this.experiences do
        fave = current.chooseBetter(fave)
      Some(fave)

  def addExperience(newExperience: Experience) =
    this.experiences += newExperience

end Category

Molemmat näistä ratkaisuista tosiaan toimivat.

Ratkaisutapojen vertailua

Koska molemmat ratkaisutavat toimivat, mutta niillä on puolensa, meille avautuu tilaisuus tarkastella lyhyesti eräitä ohjelmien laatukriteereitä.

Kriteeri

fave-ilmentymämuuttuja

Paikallinen fave ja silmukka

Käsitteellisen mallin luonnollisuus

Suosikki on luontevasti miellettävissä kategoriaolion ominaisuudeksi.

Suosikki on luontevasti miellettävissä myös favorite-toimintoon liittyväksi piirteeksi.

Toteutuksen helppous

favorite-metodin toteutus on triviaali, addExperiencen monimutkaisempi.

addExperience-metodin toteutus on triviaali, favoriten monimutkaisempi.

Suoritus- tehokkuus

Ohjelma toimii periaatteessa nopeammin ainakin silloin, jos kokemuksia on paljon ja favorite-metodia kutsutaan usein. (Tällä ei tosin ole tässä ohjelmassa minkäänlaista käytännön merkitystä.)

Tietokone käy kaikki puskurin alkiot läpi aina, kun favorite-metodia kutsutaan, joten toteutus voi olla periaatteessa hitaampi.

Muistin tarve

Kukin kategoriaolio tarvitsee hitusen lisämuistia viittaukselle suosikkiolioon. (Tällä ei tosin ole tässä ohjelmassa minkäänlaista käytännön merkitystä.)

Vain välttämätön tieto on muistissa. Suosikki voidaan aina selvittää puskurin sisältöä tutkimalla.

Koodin luettavuus ja muokattavuus

Suosikkiuteen liittyvää koodia on sekä ilmentymämuuttujien määrittelyssä, favorite-metodissa, että addExperience-metodissa. Kukin osa riippuu toisista osista. Tämä huonontaa koodin luettavuutta ja muokattavuutta vähän.

Kaikki suosikkiuteen liittyvä on keskitetysti metodissa favorite. Riippuvuudet luokan osien välillä vähän pienemmät. Koodin muokattavuus on parempi.

Suurta eroa ei tässä tapauksessa ratkaisutapojen välille voi väittää, mutta taulukon perusteella voi sanoa, että paikallista muuttujaa ja silmukkaa käyttävä ratkaisu on jossain määrin parempi. Luettavuus ja muokattavuus ovat tärkeitä!

Silmukkatehtäviä

Silmukanlukemistehtävä

Tarkastele seuraavaa ohjelmakoodia. Ensimmäinen rivi luo vektorin. Mitä muut rivit tekevät?

val sanoja = Vector("funktio", "metodi", "proseduuri", "aliohjelma", "rutiini")
var tulos = 0
for sana <- sanoja do
  if sana.length > tulos then
    tulos = sana.length
println(tulos)

Tässä läpikäytävä kokoelma ei ole puskuri vaan vektori. Periaate on silti ihan sama.

Kirjoita tähän järjestyksessä kaikki ne eri luvut, jotka tulosmuuttuja saa arvokseen koodin suorituksen aikana. Kirjoita kukin luku omalle rivilleen, allekkain. Älä toista samaa lukua useita kertoja. Muista myös muuttujan alkuperäinen arvo.

Silmukankoodaustehtävä

Avaa moduuli ForLoops ja sieltä o1.looptest-pakkauksen task1.scala. Lue tuon esimerkkiohjelman koodi ja kirjoita se uudelleen niin, että se tuottaa täsmälleen saman tulosteen kuin aiemminkin mutta for-silmukkaa käyttäen. Syntyvän koodin tulee olla selvästi alkuperäistä lyhyempi.

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

Abstrahointia, taas

Toistokäskykin on eräänlainen abstraktio: tässä tehtävässä teit annettujen konkreettisten koodirivien perusteella abstraktimman yleistyksen.

Lisää koodia: silmukat ja muuttujat

def kasvataAlkioita(luvut: Buffer[Int]) =
  for luku <- luvut do
    luku = luku + 1

Onko tuo funktion määrittely kelvollinen? (Oletetaan, että Buffer on importattu.) Päättele, kokeile tai arvaa.

Arvioi seuraavaa pientä luokkaa, jossa on silmukkaa käyttävä metodi:

class Lisaaja(val luvut: Buffer[Int]):

  var summa = 0

  // Metodi laskee puskurin nykyisten alkioiden summan ja
  // lisää tuon summan puskurin loppuun uudeksi alkioksi.
  def lisaaSumma(): Unit =
    for luku <- this.luvut do
      summa += luku
    this.luvut += summa

end Lisaaja

Mikä tai mitkä seuraavista lisaaSumma-metodia koskevista väitteistä pitävät paikkansa?

Yhteenvetoa

  • for-silmukalla voi toistaa käskyjä kullekin alkiokokoelman sisältämälle arvolle.

  • Yhdistämällä for-silmukoita ja erilaisia paikallisten muuttujien käyttötapoja saadaan aikaan metodeita, joilla voi suorittaa erilaisia toimenpiteitä kokoelmalle.

  • Lukuun liittyviä termejä sanastosivulla: silmukka, for-silmukka, iteraatio; kokoelma; muuttujan rooli.

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.

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