Kurssin viimeisimmän version löydät täältä: O1: 2024
Luku 7.1: Laiskoja kokoelmia
Tästä sivusta:
Pääkysymyksiä: Miten teen ohjelman, joka lukee etukäteen tuntemattoman määrän syötettä tai toistaa muuta toimenpidettä tuntemattoman määrän kertoja? Miten käsittelen liudan tietoalkioita yksi kerrallaan varastoimatta niitä kaikkia ensin muistiin?
Mitä käsitellään? Laiskalistat. Äärettömät listat. Evaluoimattomat eli by name -paramerit. Muodostamme myös hyvänpäiväntuttavuuden koneoppimiseen.
Mitä tehdään? Luetaan ja ohjelmoidaan.
Suuntaa antava työläysarvio:? Pari, kolme tuntia? Luku on pitkähkö, mutta iso osa siitä on vapaaehtoista lukemista.
Pistearvo: A210 + C5. (Pistearvo on korkea vaivaan nähden, koska ohjelmointitehtävien on tarkoitus olla "melkein pakollisia".)
Oheismoduulit: Sentiments (uusi), HigherOrder.
Johdanto
Lämmitellään luvun pääaihetta toisella aiheella, joka tukee tuon pääaiheen ymmärtämistä.
Lähdetään ajatuksesta, että haluamme luoda funktion fiveTimes
. Funktio palauttaa
vektorin, jossa on viisi kertaa parametriksi annetun Int
-lausekkeen arvo:
fiveTimes(100)res0: Vector[Int] = Vector(100, 100, 100, 100, 100) fiveTimes(1 + 1)res1: Vector[Int] = Vector(2, 2, 2, 2, 2)
Lauseke tulee evaluoida viidesti. Jos evaluointi tuottaa eri arvoja eri kerroilla, tulee vektoriin eri lukuja:
import scala.util.Randomimport scala.util.Random fiveTimes(Random.nextInt(100))res2: Vector[Int] = Vector(43, 55, 21, 46, 87) fiveTimes(Random.nextInt(100))res3: Vector[Int] = Vector(33, 65, 62, 31, 73)
Evaluoimattomat eli by name -parametrit
Tässä on halutusti toimiva versio fiveTimes
-funktiosta. Se on lähes identtinen
alkuperäisen yrityksen kanssa:
def fiveTimes(numberExpression: =>Int) =
Vector.tabulate(5)( anyIndex => numberExpression )
Tavalliset parametrit eli ns. by value -parametrit evaluoidaan ennen kuin funktiokutsu alkaa, kuten äskeisissä tehtävissä kerrattiin. Tällöin parametriksi välittyy evaluoidun lausekkeen arvo. Tästä poiketen by name -parametria ei evaluoida ennen funktiokutsun aloittamista. Funktiolle välitetään evaluoimaton lauseke. Tuo parametrilauseke evaluoidaan vasta silloin, kun sitä funktion rungossa käytetään (jos käytetään). Siis kesken funktion suorituksen.
Sikäli kun by name -parametria käytetään funktion rungossa useita kertoja, niin lauseke myös evaluoidaan useita kertoja:
def fiveTimes(numberExpression: =>Int) =
Vector.tabulate(5)( anyIndex => numberExpression )
tabulate
muodostaa viisialkioisen vektorin ja laittaa
kullekin indekseistä 0–4 Int
-arvon, joka saadaan evaluoimalla
numberExpression
-parametri. Jos numberExpression
on
esimerkiksi Random.nextInt(100)
, niin vektoriin tulee viisi
erikseen arvottua kokonaislukua.Jos haluat, voit ajatella evaluoimattomia parametreja eräänlaisina parametrittomina funktioina, jotka välitetään toiselle funktiolle parametriksi.
Tässä on toinen, kuvitettu esimerkki evaluoimattomasta parametrista:
Tällä kurssilla sinun ei tarvitse itse määritellä metodeita, jotka vastaanottavat
evaluoimattomia parametreja. Tulet kuitenkin käyttämään sellaisia. Siinä ei ole mitään
erityisen hankalaa, ja itse asiassa olet jo niin tehnytkin, koskapa Option
-olioiden
getOrElse
-metodin parametri on juuri tällainen evaluoimaton parametri.
getOrElse
-metodi on määritelty joko a) palauttamaan kääreen sisältö ja jättämään
parametrinsa evaluoimatta, tai b) sisällön puuttuessa evaluoimaan parametrilauseke ja
palauttamaan sen arvo. Siksi tuo metodi toimii kuten äskeinen tehtävä osoitti.
Toinen tuttu esimerkki aiheesta
Luvussa 5.1 totesimme, että logiikkaoperaattorien &&
tai ||
perään kirjoitettu osalauseke evaluoidaan vain jos operaattoria
edeltävä osalauseke ei jo määrää koko lausekkeen arvoa. Nuo
operaattorithan ovat Boolean
-olioiden metodeita (luku 5.2);
operaattoria seuraava osalauseke on käytännössä by name -parametri.
Lisää termejä
Evaluoimattomia parametreja sanotaan siis usein by name -parametreiksi ja "tavallisia" parametreja by value -parametreiksi.
Jos funktion parametri evaluoidaan vähintään kerran niin parametria voi
sanoa tiukasti evaluoiduksi (strict). fiveTimes
-funktion
parametrit ja kaikki tavalliset by value -parametrit ovat siis tiukasti
evaluoituja. Jos parametria ei välttämättä evaluoida kertaakaan, sitä voi
sanoa väljästi evaluoiduksi (non-strict); esimerkki tästä on
getOrElse
-metodin parametri.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Nyt luvun pääaiheeseen.
Haaste: määräämätön määrä toistoja
Tähän mennessä olemme tavanneet toistaa käskyjä keräämällä datan alkioiksi kokoelmaan
ja käymällä kokoelman läpi alkio kerrallaan. Kokoelman koko on rajannut toistojen määrän
etukäteen, eikä tuo raja määrity tai muutu vasta alkioita läpi käydessä. Esimerkiksi
vektoriin säilötty data on aluksi kokonaan muistissa, ja käymme sen läpi for
-silmukalla
tai korkeamman asteen metodilla, joka toistaa toimenpidettä (korkeintaan) yhtä monta
kertaa kuin kokoelmassa on alkioita.
Mutta entäpä jos tilanne onkin vaikka jokin näistä, kuten usein on?
- Haluamme pyytää käyttäjältä syötteitä ohjelman käsiteltäväksi, kunnes käyttäjä ilmaisee haluavansa lopettaa. Syötteiden määrä ei ole tiedossa etukäteen.
- Haluamme käydä läpi dataa jostakin lähteestä (tiedostosta, verkko-osoitteesta, ohjelman ohjaaman laitteen antureista tms.) käsitellen kunkin data-alkion kerrallaan. Alkioita on etukäteen tuntematon määrä, ja niitä saattaa olla niin paljon, ettei kaikkia voi ensin kerätä kokoelmaan tietokoneen muistiin kerralla.
- Käytössämme on laskutoimitus, joka tuottaa kullakin kerralla tarkemman likiarvon halutusta tuloksesta, vaikkapa Newtonin menetelmä matemaattisen funktion nollakohtien etsimiseen. Haluamme toistaa tätä toimenpidettä iteratiivisesti eli edellistä tulosta askelittain parantaen, kunnes saavutamme hyväksyttävän tarkan vastauksen. Tarvittavien tarkennusaskelten lukumäärä ei ole etukäteen tiedossa.
Vielä yleisemmin sanoen: haluamme toistaa jotakin toimenpidettä etukäteen tuntemattoman määrän kertoja. Toistojen määrä on rajoittamaton, periaatteessa ääretönkin, esimerkiksi jos käsiteltävää dataa syntyy alati lisää.
Konkreettisempi esimerkki
Laaditaan ensin leluohjelma, joka raportoi neljän merkkijonon pituudet. Tässä ei ole vielä mitään uutta.
object LengthReport extends App {
def report(input: String) = "The input is " + input.length + " characters long."
val lines = Vector("a line of text", "another", "not written by the user", "but hardcoded into the program")
lines.map(report).foreach(println)
}
Otetaan tavoitteeksi muokata ohjelmaa niin, että se toimiikin näin:
Enter some text: hello The input is 5 characters long. Enter some text: hello again The input is 11 characters long. Enter some text: third input The input is 11 characters long. Enter some text: fourth The input is 6 characters long. Enter some text: stop already The input is 12 characters long. Enter some text: stop The input is 4 characters long. Enter some text: please
Miten voimme määritellä, milloin syötteiden kysely ja raportointi loppuvat, kun emme tiedä määrää etukäteen?
Yksi tapa selviää, kunhan opit käyttämään laiskaa kokoelmaa.
Laiskalistat
LazyList
oli ennen Stream
Käytämme Scala-kielen versiota 2.13. Aiemmissa versioissa ei
ollut LazyList
-nimistä kokoelmatyyppiä, vaan vastaava tyyppi
oli nimeltään Stream
. Tuo vanha nimi esiintyy edelleen monissa
Scala-kirjoissa ja webisivuilla.
Laiskalistat (lazy-list) ovat eräänlaisia alkiokokoelmia.
Laiskalistan voi luoda alkiot luettelemalla muista kokoelmista tuttuun tapaan:
val dataaLaiskasti = LazyList(10.2, 32.1, 3.14159)dataaLaiskasti: LazyList[Double] = LazyList(<not computed>)
toString
-metodi, jota REPL käyttää, ei näytä
kokoelman sisältöä. Tämä johtuu siitä tavasta, jolla laiskalistat
toimivat — tai pikemminkin jättävät toimimatta. Siitä kerrotaan
kohta lisää.Laiskalistan voi luoda myös olemassa olevasta kokoelmasta tutulla to
-metodilla (luku 4.2):
val sanavektori = Vector("eka", "toka", "kolmas", "neljäs")sanavektori: Vector[String] = Vector(eka, toka, kolmas, neljäs) val sanatLaiskasti = sanavektori.to(LazyList)sanatLaiskasti: LazyList[String] = LazyList(<not computed>)
LazyList
-olioilla on tutut kokoelmien perustoiminnot. Tässä esimerkiksi ohitetaan
listan pari ensimmäistä alkiota ja poimitaan jäljelle jääneistä ensimmäinen.
sanatLaiskasti.drop(2).headres9: String = kolmas
Silmukkakin toimii:
for (sana <- sanatLaiskasti) { println("listan sisältöä: " + sana) }listan sisältöä: eka listan sisältöä: toka listan sisältöä: kolmas listan sisältöä: neljäs
Samoin tutut korkeamman asteen metodit:
sanatLaiskasti.filter( _.length > 4 ).map( _ + "!" ).foreach(println)kolmas! neljäs!
Mitään mullistavaa ei vielä nähty. Miten laiskalistat eroavat tutuista kokoelmista?
Loppumaton lista
Laiskalistojen erikoisuuksiin kuuluu se, että kokoelmalla ei tarvitse olla äärellistä kokoa, vaan se voi olla päättymätön. (Vrt. ääretön lukujono matematiikassa.)
Eräs tapa luoda LazyList
-olio` on continually
-tehdasmetodi. Se tuottaa loputtoman
listan:
val laiskalista = LazyList.continually("SPAM")laiskalista: LazyList[String] = LazyList(<not computed>)
Näin loimme siis kokoelman, jonka jokaisena alkiona on merkkijono "SPAM" ja jossa on
loputtomasti keskenään samanlaisia alkioita. Otetaan take
-metodilla viisi ensimmäistä
alkiota ja käydään ne läpi:
laiskalista.take(5).foreach(println)SPAM SPAM SPAM SPAM SPAM
take
palauttaa
parametrinsa mittaisen LazyList
in, joka on pätkä
alkuperäisestä.foreach
in työ ei ole loputon, koska se käy läpi vain
take
-metodin palauttaman viisialkioisen kokoelman.Alla on toinen esimerkki loputtomasta kokoelmasta. (Siinä käytetään operaattoria ++
,
joka yhdistää kokoelmat peräkkäin; luku 4.2.)
val sanoja = LazyList("Today I don't feel like doing anything", "I just wanna lay in my bed", "Don't feel like picking up my phone", "So leave a message at the tone")sanoja: LazyList[String] = LazyList(<not computed>) val sanatJaLoppu = sanoja ++ LazyList.continually("LOPPUI JO")sanatJaLoppu: LazyList[String] = LazyList(<not computed>) sanatJaLoppu.take(7).foreach(println)Today I don't feel like doing anything I just wanna lay in my bed Don't feel like picking up my phone So leave a message at the tone LOPPUI JO LOPPUI JO LOPPUI JO
LazyList
, jonka alkioina on ensin neljä
tiettyä merkkijonoa ja sitten viidettä merkkijonoa toistuvasti
mielivaltaisen monta kertaa.Miten laiskalistat käyttäytyvät?
Yllä listojen päättymättömät osat sisälsivät vain kopioita yhdestä ja samasta alkiosta. Seuraavassa esimerkissä näin ei ole. Muodostetaan loputon näennäissatunnaislukujen kokoelma:
val satunnaisia = LazyList.continually( Random.nextInt(10) )satunnaisia: LazyList[Int] = LazyList(<not computed>)
Huomaa, että äskeinen käsky ei vielä arponut ensimmäistäkään lukua. Se vain määritteli kokoelman, jonka alkiot saadaan arpomalla tarpeen mukaan.
Otetaan tuosta kokoelmasta muutama luku:
satunnaisia.take(5).foreach(println)8
9
5
6
8
Määräsimme koneen tulostamaan alkiot laiskalistasta, joka on viiteen alkioon rajattu versio loputtomasta kokoelmastamme. Jotta tulostaminen onnistuisi, on laiskalistan muodostettava nuo alkiot eli arvottava lukuja.
Tutkitaan nyt kokoelman kuvausta:
satunnaisiares10: LazyList[Int] = LazyList(8, 9, 5, 6, 8, <not computed>)
Laiskalista on muodostanut viisi ensimmäistä alkiotaan, koska oli
niiden tulostamiseksi pakko. Ne ovat nyt tallessa satunnaisia
-muuttujan
osoittamassa kokoelmassa. Myöhempiä alkioita ei ole muodostettu, kun
ei ole tarvinnut.
Jos poimimme uudestaan tuon laiskalistan alusta korkeintaan viisi alkiota, saamme aiemmin arvottuja ja listan osaksi tallennettuja lukuja:
satunnaisia.take(3).foreach(println)8 9 5 satunnaisia.take(5).foreach(println)8 9 5 6 8
Kokoelmamme on siis laittanut luvut muistiin eikä evaluoi arvontalauseketta uudelleen, kun samat vanhat alkiot kysytään taas.
Jos otamme vähän pidemmän pätkän ja katsomme sen alkiot, muodostaa laiskalista alkioita lisää tarpeen mukaan. Tämän laiskalistan tapauksessa alkioiden muodostaminen tarkoittaa, että arvotaan perään lisää lukuja:
satunnaisia.take(7).foreach(println)8 9 5 6 8 4 1 satunnaisiares11: LazyList[Int] = LazyList(8, 9, 5, 6, 8, 4, 1, <not computed>)
Mitä on laiskuus?
Tietokone ei tietenkään voi varastoida ääretöntä määrää alkioita. Laiskalistojen
keskeinen toimintaperiaate onkin, että koko kokoelmaa kaikkine alkioineen ei muodosteta
saman tien, kun LazyList
-olio syntyy. Sen sijaan alkioita muodostetaan vain sitä mukaa,
kun niitä tarvitaan.
Esimerkiksi äskeisen foreach(println)
-käskyn suorittaminen edellytti, että arvottiin
muutama satunnaisluku käyttäen lauseketta Random.nextInt(10)
muutaman kerran. Kokoelman
sisältö tuli tällöin siltä osin luoduksi, mutta muita lukuja ei arvottu.
Ylempänä tässä luvussa esiteltiin evaluoimattomat eli by name -parametrit, joiden
evaluoinnin hoitaa kutsuttu funktio eikä kutsuja. continually
-metodi saa juuri
tällaisen parametrin. continually
muodostaa laiskalistan, joka evaluoi tuon parametrin
uudestaan ja uudestaan mutta vain silloin, kun tarvitaan uusia alkioita.
Kokoelmaa, joka evaluoi alkionsa vain tarvittaessa ja pitää ne vasta siitä eteenpäin tallessa, sanotaan laiskaksi (lazy).
Tuntemattoman kokoisia kokoelmia
On mahdollista — ja hyödyllistä — käsitellä kokoelmia, joiden koko määräytyy vasta kokoelmaa käsiteltäessä.
Luodaan satunnaisia lukuja vaikkapa väliltä 0–99. Tässä esimerkissä emme kuitenkaan poimi lukuja mitään tiettyä määrää, vaan luomme ja tulostamme niitä jonkin määrän, kunnes sattuu tulemaan luku, joka on yli 90:
LazyList.continually( Random.nextInt(100) ).takeWhile( _ <= 90 ).mkString(",")res12: String = 31,84,16,45,72,81,41,36,87,19,79,62,13,60,47,45,66,58,85,15,8,9,7,30,68,41,48,80,21,78,72,27 LazyList.continually( Random.nextInt(100) ).takeWhile( _ <= 90 ).mkString(",")res13: String = 0,65,83,38,75,33,11,18,75,51,3
takeWhile
palauttaa laiskalistan, joka päättyy juuri
ennen ensimmäistä sellaista alkiota, joka ei toteuta ehtoa. Vaikka
alkuperäinen kokoelma oli ääretön, tässä rajatussa kokoelmassa ei
ole loputtomasti alkioita. Samoin kuin continually
ja take
eivät
arvo mitään lukuja, ei myöskään takeWhile
tee niin; se ainoastaan
palauttaa LazyList
-olion, joka osaa tarvittaessa arpoa lukuja
kunnes kohdataan kokoelman päättävä alkio.
mkString
tuottaa merkkijonon, jossa on kuvattu kaikki
kokoelman alkiot; tämä pakottaa muodostamaan kunkin alkion.
Äärelliselle laiskalistalle mkString
toimii kuin mille tahansa
muullekin kokoelmalle. Esimerkkimme käyttää satunnaisuutta, ja eri
kokeilukerroilla syntyy erikokoisia kokoelmia, jotka päättyvät sen
mukaan, milloin niitä läpikäydessä tulee vastaan riittävän iso luku.
Lisäesimerkki laiskasta kokoelmasta
Seuraava esimerkki osoittaa laiskuuden vielä konkreettisemmin.
Luodaan ensin apufunktio, joka tuottaa uusia alkioita ja tulostaa ilmoituksen asiasta:
var laskuri = 0laskuri: Int = 0 def tuotaAlkio() = { laskuri += 1 println("Jaaha, täytyi vaivautua tuottamaan uusi alkio: " + laskuri) laskuri }tuotaAlkio: ()Int
Muodostetaan laiskalista, jonka alkiot luodaan tuolla funktiolla. Se ei tosin vielä "viitsi" tuottaa yhtään alkiota, koska ei tarvitse.
val laiskatLuvut = LazyList.continually( tuotaAlkio() )laiskatLuvut: LazyList[Int] = LazyList(<not computed>)
Kolmen ensimmäisen alkion tutkiminen pakottaa laiskalistan kutsumaan
tuotaAlkio
ta kuten raporteista näkyy:
laiskatLuvut.take(3).mkString(",")Jaaha, täytyi vaivautua tuottamaan uusi alkio: 1 Jaaha, täytyi vaivautua tuottamaan uusi alkio: 2 Jaaha, täytyi vaivautua tuottamaan uusi alkio: 3 res14: String = 1,2,3
Niiden tutkiminen uudestaan ei vaadi, joten kokoelma laiskottelee:
laiskatLuvut.take(3).mkString(",")res15: String = 1,2,3
Pidemmälle tutkiessa on taas muodostettava lisää alkioita:
laiskatLuvut.take(5).mkString(",")Jaaha, täytyi vaivautua tuottamaan uusi alkio: 4 Jaaha, täytyi vaivautua tuottamaan uusi alkio: 5 res16: String = 1,2,3,4,5
Sanastoa: LazyList
it ovat väljiä ja laiskoja
Yllä totesimme, että laiskalistaan muodostuu uusia alkioita vain sitä mukaa, kun kyseistä alkiota tarvitaan johonkin toimenpiteeseen. Ja sikäli kun jotakin alkiota ei tarvita, ei sitä muodosteta lainkaan. Laiskalista on siis *väljästi* evaluoitu kokoelma samassa mielessä kuin by-name -parametri on väljä.
Käytimme myös termiä laiska. Kokoelman laiskuus tarkoittaa, että:
- kokoelman alkiot evaluoidaan väljästi; ja
- kokoelma pitää jo evaluoidut alkiot tallessa, jolloin sen ei tarvitse evaluoida niitä uudestaan.
Esimerkiksi satunnaislukuja arpova laiskalista siis sekä välttää turhaa arpomista että säästää itseään tulevalta työltä (arpomiselta) pitämällä jo arvotut luvut tallessa.
Uusittu versio ohjelmastamme
Seuraavaksi sovellamme laiskalistaa syötteen käsittelyyn. Periaate alkioiden muodostamisesta laiskasti vain tarpeen mukaan on tässä ratkaisevan tärkeä.
Yllä hahmottelimme tämän ohjelman, joka raportoi neljän tietyn merkkijonon pituudet.
object LengthReport extends App {
def report(input: String) = "The input is " + input.length + " characters long."
val lines = Vector("a line of text", "another", "not written by the user", "but hardcoded into the program")
lines.map(report).foreach(println)
}
Halusimme laatia tämän sijaan ohjelman, jossa kysyttyjen syötteiden määrä on etukäteen rajaamaton ja päättyy "please"-syötteeseen. Nyt kun laiskalistat ovat tuttuja, sellainen versio ohjelmasta on helposti laadittavissa. Tämä on jo melkein valmis:
object SayPlease extends App {
def report(input: String) = "The input is " + input.length + " characters long."
val inputs = LazyList.continually( readLine("Enter some text: ") )
inputs.map(report).foreach(println)
}
Ainoa tässä tehty muutos on: emme palautakaan vektoria, jossa
on jo merkkijonoja, vaan laiskalistan, joka "tuo" syötteitä ohjelman
käsiteltäväksi. Se on päättymätön merkkijonojen lista, jonka kukin
alkio saadaan kysymällä sitä käyttäjältä. Tämän käskyn suorittaminen
ei vielä kysy käyttäjältä mitään! Käsky vasta määrittelee kokoelman,
jonka alkiot synnytetään kutsumalla readLine
a aina, kun tarvitaan
uusi alkio.
map
palauttaa laiskalistallisen raportteja. Tuon kokoelman
kukin alkio muodostetaan (tarvittaessa) evaluoimalla readLine
-käsky
ja soveltamalla sen palauttamaan arvoon report
-funktiota. Tämäkään
käsky ei vielä kysy käyttäjältä mitään eikä kutsu report
-funktiota,
vaan vain valmistautuu tekemään näin, kun tai jos kokoelmasta myöhemmin
pyydetään alkioita.
foreach
määrää raporttilistan alkiot tulostettavaksi. Jotta
laiskalista voi näin käsitellä alkion, sen on ensin määritettävä tuo
alkio kysymällä syötettä käyttäjältä ja kutsumalla report
ia.
Tuloksena syntyy ohjelma, joka toistuvasti kyselee käyttäjältä syötteitä ja raportoi niiden mitat.
Tuo siis jo pitkälti toimii. Kuitenkin lopetusehto jäi vielä toteuttamatta: yllä oleva ohjelma kyselee käyttäjältä syötteitä vaikka tuomiopäivään saakka.
Lopetusehto on helppo lisätä:
object SayPlease extends App {
def report(input: String) = "The input is " + input.length + " characters long."
val inputs = LazyList.continually( readLine("Enter some text: ") )
inputs.takeWhile( _ != "please" ).map(report).foreach(println)
}
takeWhile
-käsky tuottaa lopetussanaan "please" rajatun laiskalistan,
jonka alkioita — eli käyttäjän syötteitä — ei tuoteta yhtään enempää kuin
tarvitaan. Kun lopetussana on kohdattu, lista päättyy. (Vrt. takeWhile
-esimerkki
satunnaisluvuille ylempänä.)
Tehtävä: tunteita leffa-arvosteluissa
Laatikaamme ohjelma, johon käyttäjä voi syöttää lyhyitä sanallisia kommentteja elokuvasta ja joka pyrkii arvioimaan, onko kommentti positiivinen vai negatiivinen. Arvio perustuu tuhansiin ohjelmalle tiedoston kautta syötettyihin aitoihin elokuva-arvioihin, jotka ihminen on aiemmin käsityönä sijoittanut tunneskaalalle.
Haastavammat ja työläämmät osat on tosin jo puolestasi tehty. Tässä pienessä tehtävässä vain kunnostat ohjelman käyttöliittymän.
Tehtävänanto
Tehtäväsi on noutaa Sentiments-moduuli ja täydentää sen käyttöliittymä tiedostoon
MovieSentimentApp.scala
. Käyttöliittymän tulee toimia tekstikonsolissa näin:
Please comment on a movie or hit Enter to quit: This is a masterpiece, a truly fantastic work of cinema art. I think this sentiment is positive. (Average word sentiment: 0.36.) Please comment on a movie or hit Enter to quit: I hated it. I think this sentiment is negative. (Average word sentiment: -0.41.) Please comment on a movie or hit Enter to quit: The plot had holes in it. I think this sentiment is negative. (Average word sentiment: -0.28.) Please comment on a movie or hit Enter to quit: Adam Sandler I think this sentiment is negative. (Average word sentiment: -0.80.) Please comment on a movie or hit Enter to quit: It was great. I think this sentiment is positive. (Average word sentiment: 0.04.) Please comment on a movie or hit Enter to quit: It wasn't great. I think this sentiment is negative. (Average word sentiment: -0.16.) Please comment on a movie or hit Enter to quit: It was "great". I think this sentiment is positive. (Average word sentiment: 0.04.) Please comment on a movie, or hit Enter to quit: Bye.
Aineisto, jolla analysaattorillemme opetetaan, millaiset sanat esiintyvät positiivisissa
ja mitkä negatiivisissa elokuva-arvioissa, on tiedostossa sample_reviews_from_rotten_tomatoes.txt
,
joka löytyy moduulin sisältä. Siihen ei ole aihetta nyt koskea, mutta on sivistävää
vilkaista, millaisella datalla analysaattori koulutetaan.
Itse analysaattori on valmiiksi ohjelmoitu tiedostoon SentimentAnalyzer.scala
. Jos
haluat, voit silmäillä sen dokumentaatiota ja koodiakin, mutta älä muuta nyt sitäkään.
Laadi käyttöliittymä pakkauksen o1.sentiment.ui
tiedostoon MovieSentimentApp.scala
.
Tiedosto sisältää jo käyttöliittymän palasia, mutta sinun täytyy kytkeä ne yhteen
laiskalistaa ja sen metodeita käyttäen.
Ohjeita ja vinkkejä
- Haluttu käyttöliittymä toimii pitkälti samoin kuin aiempi sanojen pituuksia raportoiva esimerkkiohjelmakin.
- Tekemistä on itse asiassa varsin vähän. Koko tehtävän voi ratkaista parilla koodirivillä, miksei yhdelläkin.
- Tehtävässä valmiina annettu analysaattori on erittäin alkeellinen esimerkki tunteiden automaattisesta tunnistamisesta (sentiment analysis) ja — määritelmästä riippuen — koneoppimisesta (machine learning). Voit lukea niistä vähän lisää tehtävän alta.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Tunneanalyysistä
Esimerkkiohjelmamme analysoi tunteita niin yksinkertaisella algoritmilla, että on vähän yllättävääkin, miten siedettävästi se suoriutuu. Se vain tutkii sille annetusta opetusdatasta, kuinka usein mikäkin sana esiintyy positiivisissa ja negatiivisissa arvioissa ja antaa näin kullekin opetusaineiston sanalle positiivisuutta kuvaavan numeroarvon. Kun käyttäjä syöttää tekstin, ohjelma analysoi sen poimimalla kaikki tuntemansa sanat ja laskemalla noiden sanojen positiivisuusarvoista keskiarvon.
Jos haluat, voit tutustua myös luokan SentimentAnalyzer
koodiin,
joka tuon tekee. Tutustuminen tosin onnistuu helpommin ysikierroksen
paikkeilla; muistutamme asiasta sitten.
Myös paljon monimutkaisempia ja parempia algoritmeja on vastaavaan tarkoitukseen laadittu. Aiheesta on järjestetty kilpailujakin; itse asiassa tehtävässämme käytetty esimerkki onkin Kaggle-ohjelmointikilpailusivustolta.
Automaattista tunnetunnistusta voidaan soveltaa esimerkiksi suositusten tuottamisessa kuluttajille ja erilaisten diagnoosien tukena. Elokuva-arvioiden arviointi ja tekstien arviointi yleensäkin on vain yksi tunnetunnistuksen haara; lisää kertoo esimerkiksi Wikipedia.
Koneoppimisesta
Koneoppiminen (machine learning) on rajussa nosteessa oleva tietojenkäsittelyn osa-alue. Siitä on tullut jatkuva puheenaihe yliopistoihin, yritysmaailmaan ja yhteiskuntaan muutenkin. Saamme lukea, miten koneet oppivat ajamaan autoa, ennustamaan säätä ja osakekursseja ja tuotteiden suosittuutta, seulomaan roskapostia, pelaamaan lautapelejä, tunnistamaan kuvista esineitä ja kasvoja ja kasvojen pienenpienistä värimuutoksista tunteita, kääntämään kieltä, diagnosoimaan tauteja ja tietoturvauhkia, arvioimaan vakuutuskorvauspyyntöjä, lajittelemaan jätteitä ja vaikka mitä muuta.
Äskeisen esimerkkiohjelmamme voi tulkita alkeelliseksi esimerkiksi koneoppimisesta, ainakin jos valitsemme riittävän laajan määritelmän tälle termille, jolle määritelmiä riittää.
Ainakin ohjelmallamme on useita koneoppimiselle tyypillisiä piirteitä:
- Ohjelma vastaanottaa opetusdataa, josta se "oppii" tietystä tarkasteltavasta ilmiöstä, joka on ohjelman erityisala. Tässä tapauksessa datana on tiedostollinen elokuva-arvioita, joista ohjelma oppii millaisia positiiviset ja negatiiviset tunteenilmaisut ovat. Yleisemmin ottaen koneoppimisen datana voi olla muunkinlaista tekstiä, kuvia, numeroita, tms.
- Ohjelma käyttää opetusdataa määrittääkseen parametrit käyttämälleen algoritmille: eri sanojen "positiivisuuslukemat". Keskeistä on, että nämä lukemat lasketaan datasta; itse ohjelmakoodi ei ota kantaa siihen, onko jokin ilmaisu positiivinen tai negatiivinen.
- Oppimaansa nojaten ohjelma kykenee arvioimaan aiemmin kohtaamattomia tilanteita, jotka vastaavat opetusdataa riittävässä määrin, tässä tapauksessa uusia elokuva-arvioita.
Yksi piirre esimerkistämme puuttuu: emme järjestelmällisesti arvioineet sitä, kuinka laadukkaasti ohjelmamme elokuva-arvioita luokittelee emmekä parantaneet analyysiä tällaisen arvion perusteella. Siitä nyt puhumattakaan, että ohjelmamme tekisi automaattisesti tällaista itsearviointia ja tehtävässään kehittymistä. Monet edistyneemmät koneoppimissovellukset keräävät lisää dataa käyttönsä aikana ja kehittyvät sen perusteella (kuten vaikkapa Googlen Quick, Draw!-peli).
Koneoppimista käsitellään monilla Aallon kursseilla. Ohjelmointi 2 tarjoaa aiheeseen johdannon ja myös useita nimenomaisesti koneoppimista käsitteleviä kursseja on tarjolla, alkaen kurssista Machine Learning: Basic Principles. Aallossa on useitakin tutkimusryhmiä, jotka soveltavat koneoppimisen menetelmiä ja kehittävät uusia.
Oliko tuo nyt sitä tekoälyä?
Päätä itse.
Tekoäly eli keinoäly (artificial intelligence, AI) on moniselitteinen ja kiistelty muotisana.
Perinteisessä merkityksessään tekoäly viittaa ihmisajattelun tarkoitukselliseen jäljittelemiseen: pyritään mallintamaan ihmismielen toimintaa ja toistamaan sen prosesseja keinotekoisesti.
Nykyisin termillä tarkoitetaan usein monenmoista ihan muutakin.
Arki- ja markkinointikielessä tekoälyksi sanotaan milloin mitäkin sovellusta, jonka aikaansaannokset ovat riittävän hienoja, että sovellusta kelpaa kutsua älykkääksi. Kyseessä on kenties jotakin sellaista, josta tulee mieleen, että "tuota tekevät yleensä ihmiset, eivät koneet" tai "enpä ole ajatellut, että tuohonkin koneet jo pystyvät". Tunneanalyysiohjelmaamme voi arkisesti sanoa tekoälyksi, jos sen arviointikyky tekee vaikutuksen.
Nykyään kun tekoälystä kirjoitetaan, on kyseessä monesti koneoppimiseen perustuva uutuussovellus, jommoisista yllä lueteltiin esimerkkejä. Ihmisen ajattelutavasta näissä sovelluksissa ollaan usein hyvin kaukana, eikä sellaiseen pyritäkään. Pikemminkin päinvastoin: ohjelmat vain muodostavat syötedatan perusteella matemaattisia malleja ja hyödyntävät tietokoneen vahvuuksia eli nopeutta, täsmällisyyttä ja väsymättömyyttä.
Kun tekoälynä markkinoidaan esimerkiksi pelien pelaamista hyvin tuloksin tai IBM:n Watsonin kaltaista kysymys ja vastaus -teknologiaa, jotkut perinteisen tekoälyn uranuurtajat tulevat vihaisiksi. (Väitteet ihmiskunnan lopusta tekoälyn armoilla voivat saada heidät hyvin vihaisiksi.) Mutta sanan merkitys taitaa olla jo pysyvästi laventunut perinteisestä. Ehkä tekoäly vielä joskus sovittelee sanaan liittyvät skismat?
Joitakin tekoälyn muotoja käsitellään kurssilla CS-E4800 Artificial Intelligence. Helsingin yliopistolla on aiheesta lyhyt, avoin johdantokurssi verkossa.
Tekoälytermistöä
Tarkkasanaiset puhuvat erikseen suppeasta (narrow) ja yleisestä (general) tekoälystä.
Suppealla tekoälyllä tarkoitetaan järjestelmiä, jotka on laadittu olemaan hyviä (tai oppimaan hyviksi) nimenomaisella rajatulla saralla. Paljon uutisoidut koneoppimisen sovellukset edustavat lähes poikkeuksetta juuri suppeaa tekoälyä.
Jos me opetamme sen tunnistamaan autoja [kuvista], niin se ei tee mitään muuta kuin tunnista autoja. Ei ole mitään mahdollisuutta, että se päätyisi tekemään jotain muuta.
—koneoppimistutkija Timo Aila (Ylen artikkelissa)
Yleinen tekoäly on vielä paljon haastavampi tavoite: älyn ei tulisi olla sidottu tiettyyn aihepiiriin, vaan sen pitäisi kyetä hahmottamaan laajoja kokonaisuuksia, oppimaan niistä ja yhdistelemään niitä. Kenties tällainen järjestelmä olisi ohjelmoitu kehittämään avukseen eri aihepiireihin sopivaa suppeaa tekoälyä.
Käytetään myös termejä heikko (weak) ja vahva (strong) tekoäly, jotka tarkoittavat suunnilleen samaa kuin suppea ja yleinen. Lisäksi niihin voi liittyä ajatus siitä, että vahvalla tekoälyllä — toisin kuin heikolla — on jonkinasteinen tietoisuus ja ehkä tunteitakin.
Tehtävä: mittausdata vuorovaikutteisesti
Treenataan vielä hieman laiskalistan käyttöä näppäimistösyötteen käsittelyyn.
Tehtävänanto
Luvussa 6.4 teit averageRainfall
-funktion, joka laski vektorissa annettujen
sademäärien (kokonaislukujen) keskiarvon, lopettaen kun syötevektorissa osutaan
mittausjakson päättävään lukuun 999999. Negatiiviset syötearvot yksinkertaisesti
ohitetaan.
Toteuta sama toiminnallisuus vuorovaikutteisena ohjelmana tiedostoon RainfallApp.scala
moduulissa HigherOrder.
Ohjelman tulee toimia täsmälleen seuraavien ajoesimerkkien mukaisesti.
Enter rainfall (or 999999 to stop): 10
Enter rainfall (or 999999 to stop): 5
Enter rainfall (or 999999 to stop): 100
Enter rainfall (or 999999 to stop): 10
Enter rainfall (or 999999 to stop): 5
Enter rainfall (or 999999 to stop): -100
Enter rainfall (or 999999 to stop): -100
Enter rainfall (or 999999 to stop): 110
Enter rainfall (or 999999 to stop): 999999
The average is 40.
Enter rainfall (or 999999 to stop): 999999
No valid data. Cannot compute average.
Enter rainfall (or 999999 to stop): -123
Enter rainfall (or 999999 to stop): -1
Enter rainfall (or 999999 to stop): 999999
No valid data. Cannot compute average.
Ohjeita ja vinkkejä
- Sinun ei tarvitse vaivata mieltäsi mahdollisuudella, että syöte
on epäkelpo, esimerkiksi tyhjä "", "laama", "3.141" tms. Voit
olettaa, että syötetty merkkijono on muunnettavissa kokonaisluvuksi
toInt
-metodilla (luku 5.2). - Yksi tapa ratkaista tehtävä on lukea kaikki syötteet laiskalistalla,
kopioida ne vektoriin (
toVector
) ja laskea tulos vektorista kuten aiemmassa sadamäärätehtävässä. Toisaalta syytä vektorin käyttöön ei ole, koska myös laiskalistaa voi käsitellä vektoreista tutuilla metodeilla.- Esimerkiksi metodeita
size
jasum
voi mainiosti kutsua laiskalistalle, kunhan lista on äärellinen. - Jos teet niin, käytä muuttujaa, joka viittaa tuohon laiskalistaan. (Varo tekemästä vahingossa useita erillisiä syötettä näppäimistöltä lukevia laiskalistoja kuten aiemmassa monivalintatehtävässä.)
- Esimerkiksi metodeita
- Huolehdi oikeinkirjoituksesta, jotta saat pisteet automaattitarkastimelta.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Valinnaista: laiskalistoihin viittaaminen ja tehokkuus
Aiemminkin on tullut esille, että pienen näköinen valinta def
- ja val
-sanojen välillä
voi olla hyvinkin merkityksellinen. Tässä valinnaisessa kappaleessa käsitellään sitä,
miten tuo valinta vaikuttaa laiskalistojen käyttöön.
val
, def
ja LazyList
ien muistintarpeet
Johdantoesimerkki
Niin kauan kuin meillä on tallessa viittaus LazyList
-olioon,
roskankerääjä ei siivoa sen alkioita pois muistista.
Tämä toteutui niissä aiemmissa esimerkeissämme, joissa meillä oli
muuttuja (val
), joka tallensi viittauksen LazyList
iin,
kuten tässäkin:
val satunnaisia = LazyList.continually( Random.nextInt(100) )satunnaisia: LazyList[Int] = LazyList(<not computed>) satunnaisia.take(5).mkString(",")res17: String = 11,70,28,1,93 satunnaisia.take(5).mkString(",")res18: String = 11,70,28,1,93
Saimme samat luvut, koska kyseessä oli yksi ja sama LazyList
-olio.
Entä jos olisimme käyttäneet muuttujan sijaan funktiota, joka
palauttaa viittauksen satunnaislukujen listaan:
def satunnaisia = LazyList.continually( Random.nextInt(100) )satunnaisia: LazyList[Int] satunnaisia.take(5).mkString(",")res19: String = 42,72,7,86,30 satunnaisia.take(5).mkString(",")res20: String = 2,2,64,87,54
Lauseke satunnaisia
on nyt parametrittoman funktion kutsu.
Aina, kun evaluoimme sen, saamme kokonaan uuden LazyList
in. Noiden
listojen alkiot ovat toisistaan aivan riippumattomat.
Koska emme pistäneet talteen viittausta, joka osoittaisi kumpaankaan luomistamme satunnaislukujen lisoista, eivät listaoliot jää muistiin. Roskankerääjä vapauttaa niiden viemän muistitilan ripeästi muuhun käyttöön.
Tehokkuudesta
Vertaillaan vielä kahta koodinpätkää. Tässä ensimmäinen, jossa
val
-muuttuja viittaa laiskalistaan:
val longListOfInputs = LazyList.continually( readSomeDataFromSomewhere() )
longListOfInputs.map(doStuffWithDataElement).foreach(println)
longListOfInputs.map(doSomethingElseWithElement).foreach(println)
longListOfInputs
jää
viittaus kaikki alkiot sisältävään kokoelmaan.LazyList
in varastoimia alkioita.Tässä identtinen koodi def
-sanalla val
in sijaan:
def longListOfInputs = LazyList.continually( readSomeDataFromSomewhere() )
longListOfInputs.map(doStuffWithDataElement).foreach(println)
longListOfInputs.map(doSomethingElseWithElement).foreach(println)
Tämä ohjelma ei pistä laiskalistaa sisältöineen talteen. Itse asiassa jo samalla, kun ohjelma käsittelee listan alkioita yksi kerrallaan, tulee kustakin käsitellystä alkiosta saman tien vapaata riistaa roskankerääjälle, joka toimii ohjelma-ajon taustalla. Varsinkin jos lista on pitkä, voi käydä niin, että alkupään alkiot on käsitelty ja poistettu muistista ennen kuin loppupää on edes muodostettu.
Riippuu tilanteesta, kumpi on parempi ratkaisu:
- Jos alkioita on suhteessa käytettävissä olevaan muistiin vähän — ja varsinkin jos samoja alkioita on tarkoitus käydä läpi useasti — niin laiskalistan tallentaminen muuttujaan on hyvä ajatus.
- Jos alkiot käydään läpi vain kerran ja kunkin
niistä voi heittää menemään heti käytön jälkeen
(kuten vaikkapa
SentimentAnalyzer
-esimerkissämme), niindef
on parempi vaihtoehto. Tällä tavoin on myös mahdollista käydä läpi suuri määrä dataa ilman, että kaikkien data-alkioiden on mahduttava tietokoneen muistiin yhtaikaa.
Kun datamäärät ovat pieniä, tällä seikalla ei yleensä ole käytännön merkitystä.
Listojen sisäisestä toteutuksesta
Listojen sisäinen toteutus perustuu ajatukseen, että kuhunkin alkioon liitetään eli linkitetään viittaus seuraavaan alkioon.
Linkitetty rakenne mahdollistaa muistin vapautumisen kuvatusti. Toisaalta se tarkoittaa sitä, että listaa on tehokasta käsitellä vain järjestyksessä, ei alkioita sieltä täältä poimien (mikä taas toimii mainiosti esimerkiksi puskureille ja vektoreille).
Linkitetyistä rakenteista ja tehokkuudesta puhutaan enemmän mm. kurssilla Ohjelmointi 2.
Lukusarjoja kätevästi
Vektori (tai puskuri tms. tiukasti evaluoitu kokoelma) ei tuohon sovi, koska kaikki vektorin alkiot pidetään muistissa ja äärettömän kokoinen vektori veisi äärettömästi muistia.
Mutta laiskalista sopii:
val positiiviset = LazyList.from(1)positiiviset: LazyList[Int] = LazyList(<not computed>)
positiiviset.take(3).foreach(println)1
2
3
Scala API:sta sattuu löytymään metodi from
, jolla voi helposti
luoda peräkkäisiä lukuja muodostavan kokoelman. Parametriksi annetaan
luku, josta aloitetaan.
Tässä edetään kymmenen kerrallaan:
val kympit = LazyList.from(0, 10)kympit: LazyList[Int] = LazyList(<not computed>) kympit.take(3).foreach(println)0 10 20
Ja tässä miinus yksi kerrallaan:
val negatiiviset = LazyList.from(-1, -1)negatiiviset: LazyList[Int] = LazyList(<not computed>)
negatiiviset.take(3).foreach(println)-1
-2
-3
Metodien yhdistelyä:
val ekaIsoNelio = LazyList.from(0).map( n => n * n ).dropWhile( _ <= 1234567 ).headekaIsoNelio: Int = 1236544
Lisämateriaalia: erilaisia LazyList
ejä
Iteroiva lista
Mainitsemisen arvoinen on myös iterate
-metodi. Se luo kokoelman,
jossa seuraava alkio saadaan edellisestä soveltamalla tiettyä
funktiota aina vain uudelleen:
val vaihteleva = LazyList.iterate(1)( x => -2 * x )vaihteleva: LazyList[Int] = LazyList(<not computed>) vaihteleva.take(4).foreach(println)1 -2 4 -8 val lallatus = LazyList.iterate("")( "la" + _ )lallatus: LazyList[String] = LazyList(<not computed>) lallatus.take(4).foreach(println) la lala lalala
Alla olevassa esimerkissä tuotetaan iterate
-metodilla laiskalista ja
toteutetaan sen avulla algoritmi, joka määrittää neliöjuuren
likiarvon käyttämättä sqrt
-kirjastofunktiota. (Koodi soveltaa Newtonin
menetelmää
positiivisen luvun neliöjuuren arvioimiseen.)
def squareRoot(n: Double) = { def isTooFar(approx: Double) = (approx * approx - n).abs > 0.0001 def nextApprox(prev: Double) = (prev + n / prev) / 2 def approximations = LazyList.iterate(1.0)(nextApprox) approximations.dropWhile(isTooFar).head }squareRoot: (n: Double)Double squareRoot(9)res21: Double = 3.000000001396984 squareRoot(654321)res22: Double = 808.9011064400888
nextApprox
-apufunktiota kutsumalla.Mielivaltainen laiskalista rekursiolla
continually
-, from
- ja iterate
-tehdasmetodeilla voi tehdä
tietynlaisia kokoelmia kätevästi. Jos niitä ei olisi tarjolla
tai jos haluamme määritellä LazyList
-olion, joka ei sovi noiden
metodien muottiin?
Voimme räätälöidä haluamamme kokoelman rekursion avulla.
Alla on yksinkertainen esimerkki rekursiivisesta eli itseensä
viittaavasta laiskalistan määrittelystä. Tämä koodi tuottaa
positiivisten lukujen sarjan (samanlaisen kuin LazyList.from(1)
).
def positiiviset(eka: Int): LazyList[Int] = eka #:: positiiviset(eka + 1)positiiviset: (eka: Int)LazyList[Int]
positiiviset(1).take(3).foreach(println)1
2
3
#::
muodostaa LazyList
in
yhdistelmänä: alkuun tulee vasemmalla
mainittu yksittäinen arvo ja perään
laitetaan oikealla mainittu laiskalista.Samalla perusidealla voi määritellä aivan minkälaisen tahansa
LazyList
in; kokeile. Myös esimerkiksi kirjastometodit from
ja continually
on määritelty juuri rekursiivisesti.
Rekursio on monipuolinen ohjelmointitekniikka, joka ei liity vain laiskalistoihin. Lisää siitä luvussa 12.1.
Tulkintatehtävä
Seuraava esimerkki on mutkikkaampi. Osaatko selvittää, mitä tämä funktio tekee ja miten se sen tekee?
Funktio ja sen osat on tarkoituksella nimetty epämääräisesti muttei muuten tarkoituksellisen harhaanjohtavasti.
Tehtävä on matemaattisluonteinen.
def mystery(limit: Int): Vector[Int] = {
import scala.math.sqrt
val odd = LazyList.from(3, 2)
def candidates = odd.takeWhile( _ <= limit )
def initialVals = odd.takeWhile( _ <= sqrt(limit).toInt )
def multiples(n: Int) = LazyList.from(n * n, 2 * n).takeWhile( _ <= limit )
def rejected = initialVals.flatMap(multiples)
val result = candidates.diff(rejected)
result.toVector
}
Lisämateriaalia: laiskat muuttujat ja "tavalliset listat"
Laiskat muuttujat
Mitä eroa on näillä?
val tulos = "laama".length
println(tulos)
println(tulos)
def tulos = "laama".length
println(tulos)
println(tulos)
Tähän mennessä opitun perusteella ero on selvä. Ensimmäinen,
val
-sanalla määritelty muuttuja, hoitaa homman (laskee
merkkijonon pituuden) heti täsmälleen yhden kerran ja pitää
tuloksen muistissa. Toinen, def
-sanalla määritelty funktio,
jättää homman aluksi tekemättä eikä tee sitä kertaakaan, ellei
funktiota kutsuta; toisaalta se tekee saman työn useasti,
jos funktiota kutsutaan useasti.
Noista koodinpätkistä jälkimmäinen siis laskee pituuden kahdesti, ensimmäinen käyttää muistia välttääkseen laskemasta samaa uudestaan.
LazyList
-olion totesimme olevan vähän kuin tästä väliltä:
se hoitaa hommansa (alkioiden muodostamisen) vasta pyydettäessä,
mutta pistää sitten tuloksen muistiin ja välttää näin tekemästä
samaa uudelleen. Tätä kutsuimme laiskaksi.
Myös muuttuja voi olla laiska. Scalassa tällainen muuttuja
määritellään sanoilla lazy val
:
lazy val tulos = "laama".lengthtulos: Int = <lazy> println(tulos)5 println(tulos)5
"laama".length"
,
tuota lauseketta ei vielä evaluoida. Laiskaan
muuttujaan kytketään evaluoimaton lauseke.Tässä toinen esimerkki. Määritellään funktio ja kaksi laiskaa muuttujaa.
def printtaaJaPalauta(luku: Int) = { println("Palautan parametrini " + luku) luku }printtaaJaPalauta: (luku: Int)Int lazy val eka = printtaaJaPalauta(1)eka: Int = <lazy> lazy val toka = printtaaJaPalauta(2)toka: Int = <lazy>
Kumpikaan muuttujamäärittelyistä ei vielä kutsunut funktiota, minkä voi todeta siitä, ettei funktion sisältämää tulostuskäskyä vielä suoritettu. Jatketaan:
if (eka > 0) eka * 10 else toka * 10Palautan parametrini 1 res23: Int = 10 if (eka > 0) eka * 10 else toka * 10res24: Int = 10
if
-käskyn ehdon evaluoiminen vaatii
eka
-muuttujalle arvon, joten tämän
laiskan muuttujan arvo määritetään
printtaaJaPalauta
-funktiota kutsumalla.
Tuloste ilmestyy näkyviin.if
-lausekkeen arvoksi saadaan eka * 10
.
eka
-muuttujalle on jo laskettu arvo,
joten sitä ei lasketa uudestaan. Funktiomme
ei tulosta toista riviä, kuten olisi käynyt,
jos eka
olisi def
eikä lazy val
.printtaaJaPalauta
-funktion lisätulosteita,
koska eka
-muuttujalla on jo arvo.toka
-muuttujan arvoa tarvittu eikä sen
arvoa ole edes määritetty.Joissakin ohjelmointikielissä, kuuluisimmin Haskellissä, kaikki muuttujat ovat laiskoja ja kaikki lausekkeet evaluoidaan väljästi. Useimmissa ohjelmointikielissä muuttujat eivät kuitenkaan ole laiskoja ja lausekkeiden evaluointi on tavallisesti tiukkaa.
Myös Scalassa tiukka evaluointi on lähtökohtana. Mutta kuten tässä luvussa olet nähnyt, ohjelmoija voi valikoidusti määritellä väljästi evaluoitavia parametreja sekä laiskoja kokoelmia ja muuttujia.
Vähän List
-luokasta eli "tavallisista listoista"
Tässä luvussa on käsitelty laiskalistoja ja Scalan luokkaa
LazyList
. On myös olemassa listoja, jotka eivät ole laiskoja.
Listat (list) ovat muuttumattomia alkiokokoelmia ja muistuttavat sikäli muun muassa vektoreita. Ne ovat yleisiä funktionaalisessa ohjelmoinnissa, ja niitä käytetään monissa Scala-ohjelmissa. (Mm. kurssilla Ohjelmointi 2 listat ovat merkittävässä roolissa.)
Listaa käytetään pitkälti niin kuin ennestään tuttuja kokoelmia. Uuden listan luominen käy esimerkiksi näin:
val tyhjaLista = List[Int]()tyhjaLista: List[Int] = List() val sanalista = List("eka", "toka", "kolmas")sanalista: List[String] = List(eka, toka, kolmas)
Tutut metodit ovat tarjolla:
sanalista.sizeres25: Int = 3 sanalista.tailres26: List[String] = List(toka, kolmas) sanalista.map( _.length )res27: List[Int] = List(3, 4, 6) val pidempi = "alkuun" :: sanalistapidempi: List[String] = List(alkuun, eka, toka, kolmas)
::
käytetään nimenomaan listoja
käsiteltäessä. Se muodostaa uuden listan, jonka
alussa on lisäalkio, ja vastaa siis kokoelmien
yleistä operaattoria +:
(luku 4.2) ja
laiskalistojen operaattoria #::
.Toteutustekniikastaan johtuen listat sopivat tehokkuusominaisuuksiltaan tilanteisiin, joissa joko käsitellään nimenomaan kokoelman alkupään alkioita tai käydään kokoelmaa läpi järjestyksessä. Toisaalta lista ei yleensä ole hyvä valinta, jos tarkoitus on poimia mielivaltaisia alkioita jostakin päin kokoelmaa esimerkiksi indeksin perusteella.
Yhteenvetoa
- Laiskalista (
LazyList
) on kokoelma, jonka alkioita ei muodosteta etukäteen vaan vain tarvittaessa.- Laiskalista sopii käytäväksi läpi järjestyksessä.
- Laiskalistan alkiot voi käsitellä yksi kerrallaan varastoimatta niitä kaikkia yhtaikaisesti muistiin tai edes tietämättä etukäteen, paljonko alkioita tulee olemaan.
- Tutummista kokoelmatyypeistä poiketen laiskalista
voi olla äärellinen tai ääretön. Äärettömästä
laiskalistasta voi rajata tarkasteltavaksi
oleellisen osan esimerkiksi
take
- taitakeWhile
-metodilla. - Laiskalistan voi muodostaa esimerkiksi toistamalla
tiettyä toimenpidettä (
continually
), tuottamalla lukuja järjestyksessä (from
), soveltamalla toimenpidettä toistuvasti aiempaan tulokseen (iterate
) ja muillakin tavoilla.
- Evaluoimattomat parametrit eli by name -parametrit välitetään
kutsutulle funktiolle evaluoimattomina lausekkeina toisin kuin
tavalliset (by value) parametrit, jotka evaluoidaan ensin.
- Evaluoimattoman parametrilausekkeen vastaanottaja voi evaluoida sen kerran, ei kertaakaan tai useita kertoja sen mukaan kuin on tarkoituksenmukaista.
- Tällaiset parametrit ovat apunamme mm. laiskalistoja muodostaessamme.
- Lukuun liittyviä termejä sanastosivulla: laiskalista (eli virta); evaluoimaton parametri eli by name -parametri; tiukka, väljä ja laiska evaluointi. Nämäkin mainittiin: koneoppiminen, tekoäly.
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 ja Nikolas Drosdek yhteistyössä Juha Sorvan, Otto Seppälän, Arto Hellaksen ja muiden kanssa.
Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.
Lisäkiitokset tähän lukuun
Sademäärätehtävä on muunnelma Elliot Solowayn klassikkotehtävästä.
Tunneanalyysitehtävä on muunnelma Eric D. Manleyn and Timothy M. Urnessin muotoilemasta ohjelmointitehtävästä, joka perustuu alun perin Kaggle-sivuston ohjelmointikilpailuun.
Laulun sanat ovat Bruno Marsin biisistä.