Kurssin viimeisimmän version löydät täältä: O1: 2024
Luku 7.1: Virtoja
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? Äärelliset ja äärettömät virrat. 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 tuntia? Luku on pitkähkö, mutta iso osa siitä on vapaaehtoista lukemista.
Pistearvo: A190. (Pistearvo on korkea vaivaan nähden, koska ohjelmointitehtävien on tarkoitus olla "melkein pakollisia".)
Oheisprojektit: Sentiments (uusi), HigherOrder.
Johdanto
Lämmitellään luvun pääaihetta, virtoja, toisella aiheella, joka tukee sen 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 4.4 totesimme, että logiikkaoperaattorien &&
tai ||
perään kirjoitettu osalauseke evaluoidaan vain, mikäli operaattoria
edeltävä osalauseke ei jo määrää koko lausekkeen arvoa. Nuo
operaattorithan ovat Boolean
-olioiden metodeita (luku 4.5);
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.
Nyt luvun pääaiheeseen.
Haaste: määräämätön määrä toistoja
Tähän mennessä olemme tavanneet toistaa käskyjä siten, että keräämme käsiteltävän datan
alkioiksi kokoelmaan, jota sitten käymme 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 koko ajan 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."
def 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 virtoja.
Alkioita virrassa
Virrat (stream) ovat eräänlaisia alkiokokoelmia. Sana tulee ajatuksesta, että virta vähitellen "tuo" alkioita käsiteltäväksi.
Virtoja voi luoda alkiot luettelemalla kuten muitakin kokoelmia:
val datavirta = Stream(10.2, 32.1, 3.14159)datavirta: Stream[Double] = Stream(10.2, ?)
toString
-metodi, jota REPL käyttää, ei näytä koko
virtaa. Tämä on seuraus siitä tavasta, jolla virrat toimivat ja
josta kerrotaan kohta lisää.Toinen tapa on metodi toStream
, jolla virta luodaan olemassa olevan kokoelman perusteella
(vrt. toVector
, toBuffer
; luku 4.1):
val sanavektori = Vector("eka", "toka", "kolmas", "neljäs")sanavektori: Vector[String] = Vector(eka, toka, kolmas, neljäs)
val sanavirta = sanavektori.toStreamsanavirta: Stream[String] = Stream(eka, ?)
Virroilla on tutut kokoelmien perustoiminnot. Tässä esimerkiksi ohitetaan virran pari ensimmäistä alkiota ja poimitaan jäljelle jääneistä ensimmäinen.
sanavirta.drop(2).headres9: String = kolmas
Silmukkakin toimii:
for (sana <- sanavirta) { println("virran sisältöä: " + sana) }virran sisältöä: eka virran sisältöä: toka virran sisältöä: kolmas virran sisältöä: neljäs
Samoin tutut korkeamman asteen metodit:
sanavirta.filter( _.length > 4 ).map( _ + "!" ).foreach(println)kolmas! neljäs!
Mitään mullistavaa ei vielä nähty. Miten virrat eroavat tutuista kokoelmista?
Loppumaton virta
Virtojen erikoisuuksiin kuuluu se, että virralla ei tarvitse olla äärellistä kokoa, vaan virta voi olla päättymätön (vrt. ääretön lukujono matematiikassa).
Eräs tapa luoda virta on continually
-tehdasmetodi. Se tuottaa loputtoman virran:
val virta = Stream.continually("Olavi")virta: Stream[String] = Stream(Olavi, ?)
Näin loimme siis kokoelman, jonka jokaisena alkiona on merkkijono "Olavi" ja jossa on
loputtomasti keskenään samanlaisia alkioita. Otetaan take
-metodilla viisi ensimmäistä
alkiota ja käydään ne läpi:
virta.take(5).foreach(println)Olavi Olavi Olavi Olavi Olavi
take
palauttaa
parametrinsa mittaisen virran, joka on pätkä alkuperäisestä
virrasta.foreach
in työ ei ole loputon, koska se käy läpi vain
take
-metodin palauttaman viisialkioisen virran.Alla on toinen esimerkki loputtomasta virrasta. (Siinä käytetään operaattoria ++
, joka
yhdistää kokoelmat peräkkäin; luku 4.1.)
val sanoja = Vector("Sinutkin mukaani haluaisin", "sen virran oikkuihin suostumaan", "Satamat kanssasi unohtaisin", "ja jäisin aalloille asumaan")sanoja: Vector[String] = Vector(Sinutkin mukaani haluaisin, sen virran oikkuihin suostumaan, Satamat kanssasi unohtaisin, ja jäisin aalloille asumaan) def sanatJaLoppu = sanoja.toStream ++ Stream.continually("LOPPUI JO")sanatJaLoppu: Stream[String] sanatJaLoppu.take(7).foreach(println)Sinutkin mukaani haluaisin sen virran oikkuihin suostumaan Satamat kanssasi unohtaisin ja jäisin aalloille asumaan LOPPUI JO LOPPUI JO LOPPUI JO
toStream
-kutsu, esimerkkikoodimme ei
toimisi. Mitä silloin tapahtuisi? Voit selvittää kokeilemalla,
mutta tallenna keskeneräiset työt Eclipsessä ensin.Yllä virtojen päättymättömät osat sisälsivät vain kopioita yhdestä ja samasta alkiosta. Seuraavassa esimerkissä näin ei ole. Muodostetaan näennäissatunnaislukujen virta.
val satunnaisia = Stream.continually( Random.nextInt(10) )satunnaisia: Stream[Int] = Stream(8, ?)
Otetaan virrasta muutama luku:
satunnaisia.take(5).foreach(println)8 9 5 6 8
Satunnaisia lukuja väliltä 0–99 kunnes sattuu tulemaan yli 90:
Stream.continually( Random.nextInt(100) ).takeWhile( _ <= 90 ).mkString(",")res10: 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 Stream.continually( Random.nextInt(100) ).takeWhile( _ <= 90 ).mkString(",")res11: String = 0,65,83,38,75,33,11,18,75,51,3
takeWhile
palauttaa virran, joka päättyy juuri ennen ensimmäistä
alkiota, joka ei toteuta tiettyä ehtoa. Vaikka alkuperäinen virta
oli ääretön, tässä rajatussa virrassa ei ole loputtomasti alkioita.
Samoin kuin continually
ja take
eivät vielä arponeet virtaa
täyteen satunnaislukuja, ei myöskään takeWhile
tee niin; se
ainoastaan palauttaa virran, joka osaa arpoa lukuja kunnes
kohdataan virran päättävä alkio.mkString
tuottaa merkkijonon, jossa ovat kaikki virran alkiot.
Äärettömälle virralle kutsuttuna se kuluttaisi tietokoneen
muistiresurssin ja katkeaisi virheeseen, mutta äärelliselle
virralle se toimii kuin mille tahansa kokoelmalle. Esimerkkimme
käyttää satunnaisuutta, ja eri kokeilukerroilla syntyy erilaisia
virtoja, jotka päättyvät sen mukaan, milloin niitä läpikäydessä
tulee vastaan riittävän iso luku.Virtojen toimintaperiaate
Tietokone ei tietenkään voi varastoida ääretöntä määrää alkioita. Virtojen keskeinen
toimintaperiaate onkin, että koko virtaa kaikkine alkioineen ei muodosteta saman tien,
kun virtaolio syntyy. Sen sijaan alkioita muodostetaan vain sitä mukaa, kun niitä
tarvitaan. Esimerkiksi äskeisen mkString
-käskyn suorittaminen edellytti, että arvottiin
muutama satunnaisluku käyttäen lauseketta Random.nextInt(100)
muutaman kerran. Virran
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 ja muodostaa kokoelman, joka evaluoi tuon parametrin uudestaan
ja uudestaan mutta vain silloin, kun virrasta pyydetään uusia alkioita.
Periaate virran sisällön määrittelemisestä vain tarpeen mukaan eli laiskasti on ratkaisevan tärkeä, kun seuraavaksi sovellamme virtaa syötteen käsittelyyn.
Uusittu versio ohjelmastamme
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."
def 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 virrat 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."
def inputs = Stream.continually( readLine("Enter some text: ") )
inputs.map(report).foreach(println)
}
readLine
a aina, kun tarvitaan uusi alkio.map
tuottaa "raporttivirran", jonka 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 virrasta myöhemmin
pyydetään alkioita.foreach
määrää raporttivirran alkiot tulostettavaksi. Jotta
alkion voi tulostaa, se on ensin määritettävä 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."
def inputs = Stream.continually( readLine("Enter some text: ") )
inputs.takeWhile( _ != "please" ).map(report).foreach(println)
}
takeWhile
-käsky tuottaa lopetussanaan "please" rajatun virran.
Tuon virran alkioita — eli käyttäjän syötteitä — ei tuoteta
yhtään enempää kuin tarvitaan. Kun lopetussana on kohdattu, virta
päättyy. (Vrt. takeWhile
-esimerkki satunnaislukujen virralle
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 kyseinen kommentti positiivinen vai negatiivinen. Arvio perustuu tuhansiin ohjelmalle aiemmin tiedostossa syötettyihin aitoihin elokuva-arvioihin, jotka ihminen on 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 projekti Sentiments 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 projektin 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 virtaa
ja sen metodeita käyttäen.
Ohjeita ja vinkkejä
- Haluttu käyttöliittymä toimii pitkälti samoin kuin tätä tehtävää edeltävä esimerkkiohjelmakin.
- Tekemistä on itse asiassa varsin vähän. Koko tehtävä on mahdollista 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.
Palauttaminen
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Tunneanalyysistä
Esimerkkiohjelmamme analysoi teksteissä esiintyviä 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 (tekstiä, kuvia, lukuja tms.), 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.
- 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ä kehittäneet ohjelmaa 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 tätä aihetta käsitteleviä kursseja on tarjolla, alkaen kurssista Machine Learning: Basic Principles. Aallossa on useitakin tutkimusryhmiä, jotka soveltavat koneoppimisen menetelmiä ja kehittävät uusia. Helsingin yliopistolla on aiheesta lyhyt, avoin johdantokurssi verkossa.
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ä "enpä ole ajatellut että tuohonkin koneet jo pystyvät" tai "tuota tekevät yleensä ihmiset, eivät koneet". Tunneanalyysiohjelmaamme voi arkisesti sanoa tekoälyksi, jos sen arviontikyky tekee vaikutuksen.
Nykyään kun tekoälystä kirjoitetaan, on kyseessä monesti koneoppimiseen perustuva uutuussovellus, jollaisista 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?
Tekoälytermistöä
Tarkkasanaiset puhuvat erikseen suppeasta (narrow) ja yleisestä (general-purpose) 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ä.
Yleinen tekoäly on vielä paljon haastavampi tavoite: tekoälyn ei tulisi olla sidottu tiettyyn aihepiiriin, vaan sen pitäisi kyetä hahmottamaan laajoja kokonaisuuksia, oppimaan niistä ja yhdistelemään niitä. Kenties se olisi ohjelmoitu kehittämään avukseen eri aihepiireihin sopivia suppeita tekoälyjä.
Käytetään myös termejä heikko (weak) ja vahva (strong) tekoäly, jotka tarkoittavat suunnilleen samaa kuin suppea ja yleinen tekoäly. Lisäksi niihin voi liittyä ajatus siitä, että vahvalla tekoälyllä — toisin kuin heikolla — on jonkinasteinen tietoisuus ja ehkä tunteitakin.
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)
Tehtävä: mittausdata vuorovaikutteisesti
Treenataan vielä hieman virran käyttöä näppäimistösyötteen käsittelyyn.
Tehtävänanto
Luvussa 6.3 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
projektissa 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 4.5). - Yksi tapa ratkaista tehtävä on lukea kaikki syötteet virralla,
laittaa virran sisältö vektoriin (
toVector
) ja laskea tulos siitä kuten aiemmassa sadamäärätehtävässä. Toisaalta syytä vektorin käyttöön ei ole, koska myös virtaa voi käsitellä vektoreista tutuilla metodeilla.- Esimerkiksi metodeita
size
jasum
voi mainiosti kutsua virralle, kunhan virta on äärellinen. - Jos kutsut noita metodeita virralle, käytä muuttujaa, joka viittaa tuohon virtaan.
- Esimerkiksi metodeita
- Huolehdi oikeinkirjoituksesta, jotta saat pisteet automaattitarkastimelta.
Palauttaminen
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Lukuvirtoja 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 virta sopii:
def positiiviset = Stream.from(1)positiiviset: Stream[Int]
positiiviset.take(3).foreach(println)1
2
3
from
, jolla voi helposti
luoda peräkkäisiä lukuja "kuljettavan" virran. Parametriksi
annetaan luku, josta aloitetaan.Tässä edetään kymmenen kerrallaan:
def kympit = Stream.from(0, 10)kympit: Stream[Int] kympit.take(3).foreach(println)0 10 20
Ja tässä miinus yksi kerrallaan:
def negatiiviset = Stream.from(-1, -1)negatiiviset: Stream[Int] negatiiviset.take(3).foreach(println)-1 -2 -3
Metodien yhdistelyä:
val ekaIsoNelio = Stream.from(0).map( n => n * n ).dropWhile( _ <= 1234567 ).headekaIsoNelio: Int = 1236544
Lisämateriaalia: erilaisia virtoja
Iteratiivinen virta
Mainitsemisen arvoinen on myös iterate
-metodi. Se luo virran,
jossa seuraava alkio saadaan edellisestä soveltamalla tiettyä
funktiota aina vain uudelleen:
def vaihteleva = Stream.iterate(1)( x => -2 * x )vaihteleva: Stream[Int] vaihteleva.take(4).foreach(println)1 -2 4 -8 def lallatus = Stream.iterate("")( "la" + _ )lallatus: Stream[String] lallatus.take(4).foreach(println) la lala lalala
Alla olevassa esimerkissä tuotetaan iterate
-metodilla virta ja
toteutetaan tuon virran 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 streamOfApproximations = Stream.iterate(1.0)(nextApprox) streamOfApproximations.dropWhile(isTooFar).head }squareRoot: (n: Double)Double squareRoot(9)res12: Double = 3.000000001396984 squareRoot(654321)res13: Double = 808.9011064400888
nextApprox
-apufunktiota kutsumalla.Mielivaltainen virta rekursiolla
continually
-, from
- ja iterate
-metodeilla voi tehdä
tietynlaisia virtoja kätevästi. Jos niitä ei olisi tarjolla tai
jos haluamme määritellä virran, joka ei sovi noiden tehdasmetodien
muottiin, niin voimme räätälöidä haluamamme virran rekursion avulla.
Alla on yksinkertainen esimerkki rekursiivisesta eli itseensä
viittaavasta virran määrittelystä, joka tuottaa positiivisten
lukujen sarjan (samanlaisen kuin Stream.from(1)
).
def positiiviset(eka: Int): Stream[Int] = eka #:: positiiviset(eka + 1)positiiviset: (eka: Int)Stream[Int]
positiiviset(1).take(3).foreach(println)1
2
3
#::
muodostaa virran
yhdistelmänä: alkuun tulee vasemmalla
mainittu yksittäinen arvo ja perään
laitetaan oikealla mainittu virta.Samalla perusidealla voi määritellä aivan minkälaisen tahansa virran;
kokeile. Myös esimerkiksi kirjastometodit from
ja continually
on
määritelty juuri rekursiivisesti.
Rekursio on monipuolinen ohjelmointitekniikka, joka ei liity vain virtoihin. Lisää siitä luvussa 11.2.
Tulkintatehtävä
Seuraava virtaesimerkki 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 = Stream.from(3, 2)
def candidates = odd.takeWhile( _ <= limit )
def initialVals = odd.takeWhile( _ <= sqrt(limit).toInt )
def multiples(n: Int) = Stream.from(n * n, 2 * n).takeWhile( _ <= limit )
def rejected = initialVals.flatMap(multiples)
val result = candidates.diff(rejected)
result.toVector
}
Lisämateriaalia: virtojen ominaisuuksia
Mitä eroa on näillä?
val tulos = "laama".length
println(tulos)
println(tulos)
def tulos = "laama".length
println(tulos)
println(tulos)
Ero on tietysti se, että ensimmäinen laskee merkkijonon pituuden, pistää tuloksen
talteen (koska val
) ja tulostaa sen kahdesti. Toinen laskee merkkijonon pituuden
aina pyydettäessä (koska def
), tässä tapauksessa kahdesti. Ensimmäinen käyttää
muistia välttääkseen laskemasta samaa uudestaan.
Miten tuo liittyy virtoihin?
Virrat ovat väljiä ja laiskoja
Ylempänä totesimme, että virran kaikkia alkioita ei välttämättä evaluoida lainkaan: virta on väljästi evaluoitu kokoelma. Totesimme myös, että virtaan muodostuu uusia alkioita vain sitä mukaa, kun kyseistä alkiota tarvitaan johonkin toimenpiteeseen.
Palataan tähän esimerkkiin:
val satunnaisia = Stream.continually( Random.nextInt(100) )satunnaisia: Stream[Int] = Stream(12, ?)
satunnaisia.take(5).mkString(",")res14: String = 12,62,28,14,31
Random.nextInt(100)
arpoakseen uuden alkion vasta kun tai jos tuota alkiota tarvitaan.
Tässä esimerkissä se tapahtuu viidesti, kun mkString
illä
määrätään muodostettavaksi merkkijono virran viidestä alkiosta,
mikä pakottaa virtaolion evaluoimaan alkioita muodostavan
arpomiskäskyn toistuvasti.Ja nyt tarkkana: virran jatkoksi muodostuu siis uusia alkioita vain tarvittaessa. Entä jos jo aiemmin muodostettua alkiota käytetään uudestaan?
satunnaisia.take(5).mkString(",")res15: String = 12,62,28,14,31 satunnaisia.take(5).mkString(",")res16: String = 12,62,28,14,31 satunnaisia.take(10).mkString(",")res17: String = 12,62,28,14,31,27,79,18,78,43
Sanomme, että virta ei ole vain väljästi evaluoitu kokoelma vaan lisäksi laiska (lazy): se ei ainoastaan jätä muodostamatta sellaisia alkioita, joita ei ole tarvittu, vaan myös säästää itseään tulevalta työltä (tässä: arpomiselta) pitämällä jo muodostetut alkiot tallessa.
Seuraava esimerkki osoittaa laiskuuden vielä konkreettisemmin.
Luodaan ensin funktio, joka tuottaa uusia alkioita ja tulostaa ilmoituksen asiasta:
var laskuri = 0laskuri: Int = 0 def tuotaAlkio() = { val uusiAlkio = laskuri println("Jaaha, täytyi vaivautua tuottamaan uusi alkio: " + uusiAlkio) laskuri += 1 uusiAlkio }tuotaAlkio: ()Int
Muodostetaan virta, jonka alkiot luodaan tuolla funktiolla:
val lukuvirta = Stream.continually( tuotaAlkio() )Jaaha, täytyi vaivautua tuottamaan uusi alkio: 0 lukuvirta: Stream[Int] = Stream(0, ?)
toString
-metodia, joka laittaa ensimmäisen alkion
näkyviin. (Myöhemmissä Scala-versioissa tämä tulee
muuttumaan, eikä virta tuota ensimmäistäkään alkiota ennen
kuin pyydetään.)Viiden ensimmäisen alkion tutkiminen vaatii uusia tuotaAlkio
-kutsuja:
lukuvirta.take(5).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 Jaaha, täytyi vaivautua tuottamaan uusi alkio: 4 res18: String = 0,1,2,3,4
Niiden tutkiminen uudestaan ei vaadi, joten virta laiskottelee:
lukuvirta.take(5).mkString(",")res19: String = 0,1,2,3,4
Virta pidemmälle tutkiessa on taas muodostettava lisää alkioita:
lukuvirta.take(10).mkString(",")Jaaha, täytyi vaivautua tuottamaan uusi alkio: 5 Jaaha, täytyi vaivautua tuottamaan uusi alkio: 6 Jaaha, täytyi vaivautua tuottamaan uusi alkio: 7 Jaaha, täytyi vaivautua tuottamaan uusi alkio: 8 Jaaha, täytyi vaivautua tuottamaan uusi alkio: 9 res20: String = 0,1,2,3,4,5,6,7,8,9
val
, def
ja virtojen muistintarpeet
Niin kauan kuin meillä on tallessa viittaus virtaan, roskankerääjä ei
siivoa virran alkioita pois muistista. Äskeisissä esimerkeissä tämä toteutui, koska
meillä oli muuttuja (val
), joka tallensi viittauksen luotuun virtaolioon. Entä jos
ei olisi ollut?
def satunnaisia = Stream.continually( Random.nextInt(100) )satunnaisia: Stream[Int] satunnaisia.take(5).mkString(",")res21: String = 42,72,7,86,30 satunnaisia.take(5).mkString(",")res22: String = 2,2,64,87,54
satunnaisia
on nyt parametrittoman funktion kutsu.
Aina, kun evaluoimme sen, saamme kokonaan uuden virran.Koska emme pistäneet talteen viittausta, joka osoittaisi kumpaankaan luomistamme satunnaislukujen virroista, eivät virtaoliot jää muistiin. Roskankerääjä vapauttaa ripeästi niiden viemän muistitilan muuhun käyttöön.
Vertaillaan vielä kahta koodinpätkää. Tässä ensimmäinen, jossa val
-muuttuja viittaa
virtaan:
val longStreamOfInputs = Stream.continually( readSomeDataFromSomewhere() )
longStreamOfInputs.map(doStuffWithDataElement).foreach(println)
longStreamOfInputs.map(doSomethingElseWithElement).foreach(println)
longStreamOfInputs
jää
viittaus kaikki alkiot sisältävään virtaan.Tässä identtinen koodi def
-sanalla val
in sijaan:
def longStreamOfInputs = Stream.continually( readSomeDataFromSomewhere() )
longStreamOfInputs.map(doStuffWithDataElement).foreach(println)
longStreamOfInputs.map(doSomethingElseWithElement).foreach(println)
Tämä ohjelma ei tallenna virtaa sisältöineen mihinkään. Itse asiassa jo samalla, kun se käsittelee virran alkioita yksi kerrallaan, tulee kustakin käsitellystä alkiosta saman tien vapaata riistaa roskankerääjälle, joka toimii ohjelma-ajon taustalla. Varsinkin jos virta on pitkä, voi käydä niin, että alkupään alkiot on käsitelty ja poistettu muistista ennen kuin loppupää on edes muodostettu.
Voi siis olla merkitystä sillä, tallennammeko virran muuttujaan vai emme. 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 virran 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ä.
Virtojen sisäinen toteutus perustuu ajatukseen, että kuhunkin alkioon liitetään viittaus seuraavaan alkioon. Linkitetty rakenne mahdollistaa muistin vapautumisen kuvatusti; toisaalta se tarkoittaa sitä, että virtaa 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.
Laiskat muuttujat
Yllä vertailimme näitä määrittelyjä:
val tulos = "laama".length
def tulos = "laama".length
Ensimmäinen, muuttuja, hoitaa homman (laskee pituuden) heti täsmälleen yhden kerran ja pitää tuloksen muistissa. Toinen, 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.
Virran 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ä sanoimme 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, vaikka
tuo muuttuja if
-käskyssä esiintyykin.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 ainakin pääsääntöisesti 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 ja kokoelmia sekä laiskoja muuttujia.
Yhteenvetoa
- Virta (
Stream
) on kokoelma, jonka kaikkia alkioita ei muodosteta etukäteen vaan vain tarvittaessa.- Virta sopii käytäväksi läpi järjestyksessä.
- Virran 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 virta voi
olla äärellinen tai ääretön. Äärettömästä virrasta
voi rajata tarkasteltavaksi oleellisen osan
esimerkiksi
take
- taitakeWhile
-metodilla. - Virran voi muodostaa esimerkiksi toistamalla
tiettyä toimenpidettä (
continually
), tuottamalla virtaan 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. virtoja muodostaessamme.
- Lukuun liittyviä termejä sanastosivulla: virta; evaluoimaton parametri eli by name -parametri; tiukka, väljä ja laiska evaluointi. (Myös: 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!
Kierrokset 1–13 ja niihin liittyvät tehtävät ja viikkokoosteet on laatinut Juha Sorva.
Kierrokset 14–20 on laatinut Otto Seppälä. Ne eivät ole julki syksyllä, mutta julkaistaan ennen kuin määräajat lähestyvät.
Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.
Tehtävien automaattisen arvioinnin ovat toteuttaneet Riku Autio, Jaakko Kantojärvi, Teemu Lehtinen, Timi Seppälä, Teemu Sirkiä ja Aleksi Vartiainen.
Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.
Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista ovat suunnitelleet Juha Sorva ja Teemu Sirkiä. Niiden teknisen toteutuksen ovat tehneet Teemu Sirkiä ja Riku Autio käyttäen Teemun toteuttamia Jsvee- ja Kelmu-työkaluja.
Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset on laatinut Juha Sorva.
O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.
Opetustapa, jossa käytämme O1Libraryn työkaluja (kuten Pic
) yksinkertaiseen graafiseen
ohjelmointiin on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi
oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.
Oppimisalusta A+ on luotu Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Pääkehittäjänä toimii tällä hetkellä Jaakko Kantojärvi, jonka lisäksi järjestelmää kehittävät useat tietotekniikan ja informaatioverkostojen opiskelijat.
Kurssin tämänhetkinen henkilökunta on kerrottu luvussa 1.1.
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.
Virtalaulun sanat ovat Marja Mattlarin.