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

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

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. Halutessasi voit käydä tässä välissä tekemässä tuon tehtävän tai katsoa sen vastauksen tästä:

EnglishAuction-luokan toteutus

Näytä koodiPiilota koodi

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

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

  def daysLeft = this.remainingDays

  def advanceOneDay() = {
    if (this.isOpen) {
      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)
      this.startingPrice
    else
      min(this.secondHighest.limit + 1, this.highest.limit)

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

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

  override def toString = this.description
}
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 tänne.
}
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 = new 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 = new EnglishAuction("A glorious handbag", 100, 14)bag: EnglishAuction = A glorious handbag
house.addItem(bag)val camera = new EnglishAuction("Nikon COOLPIX", 150, 3)camera: EnglishAuction = Nikon COOLPIX
house.addItem(camera)house.addItem(new 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 voi havaita 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 toteutetuksi halutunlaiseksi.

nextDay-metodi

Suunnitellaan algoritmi nextDay-metodille:

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

Tarkennetaan:

def nextDay() = {
  Vuorotellen jokaiselle alkiolle tämän olion huutokauppaluettelossa:
    - 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:
    - 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) {
    current.advanceOneDay()
  }
}
Tämän voi lukea "for each item current in this.items" tai "tee vuorotellen jokaiselle alkiolle current, joka löytyy kokoelmasta this.items".
Tässä tulee määritellyksi muuttuja, jolle voimme valita nimen; tässä olemme 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.

for-silmukan rakenne

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

for (muuttuja <- alkiokokoelma) {
  Tee jotain alkiolla, joka on varastoitu muuttujaan.
}

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 11.1), lista (luku 7.1) tai hakurakenne (luku 8.4).

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-fi1.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) {
  // luokan muita osia
}

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) {
  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 hyötykäyttöjä, 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:
    - 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:
    - 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) {
    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

}
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 = new AuctionHouse("Huutelu.net")
val esine = new 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:
    - 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:
    - 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 ne esineet, jotka parametriarvona annettu asiakas on joko jo ostanut tai 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:
    - 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.2, ja AuctionHouse-moduuli syntyy uudelleen entistä ehompana luvussa 7.5.

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:
    - 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:
    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) {
    if (current.price > priciestSoFar.price) {
      priciestSoFar = current
    }
  }
  priciestSoFar
}

Mitä unohtui?

Metodihan oli speksattu 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 metodin kutsuminen aiheuttaa ajonaikaisen poikkeustilanteen, jos items on tyhjä (koska tällöin edes indeksillä 0 ei ole alkiota). Ongelma on analoginen sen kanssa, 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) {
    None
  } else {
    var priciestSoFar = this.items.head
    for (current <- this.items) {
      if (current.price > priciestSoFar.price) {
        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)
    }
  }

}

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) {
      None
    } else {
      var fave = this.experiences(0)
      for (current <- this.experiences) {
        fave = current.chooseBetter(fave)
      }
      Some(fave)
    }
  }

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

}

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ä.) Mitään "ei-välttämätöntä" tietoa ei ole 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 voitaneen 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. Mieti, mitä se tekee vektorilla, joka luodaan ensimmäisellä rivillä.

val sanoja = Vector("funktio", "metodi", "proseduuri", "aliohjelma", "rutiini")
var tulos = 0
for (sana <- sanoja) {
  if (sana.length > tulos) {
    tulos = sana.length
  }
}
println(tulos)
Tässä läpikäytävä kokoelma ei ole puskuri vaan vektori. Periaate on kuitenkin 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.

Silmukankoodaustehtävä

Avaa moduuli ForLoops ja sieltä yksittäisolio o1.looptest.Task1. 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 tulisi 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) {
    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 lisays: Int) {

  var indeksi = 0

  // Muokkaa annettua puskuria lisäten kuhunkin alkioon saman verran.
  def lisaa(luvut: Buffer[Int]) = {
    for (luku <- luvut) {
      luvut(indeksi) = luku + this.lisays
      indeksi += 1
    }
  }
}

Mikä tai mitkä seuraavista lisaa-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, 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 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 tällä hetkellä 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 ovat luoneet Nikolai Denissov, Olli Kiljunen, Nikolas Drosdek, Styliani Tsovou, Jaakko Närhi ja Paweł Stróżański yhteistyössä Juha Sorvan, Otto Seppälän, Arto Hellaksen ja muiden 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...