Luku 1.6: Aliohjelmien käyttö
Johdanto
Tämä ja kaksi seuraavaa lukua käsittelevät aliohjelmia (subprograms).
Aliohjelma on toteutus jollekin tietylle toiminnolle. Aliohjelma voi esimerkiksi laskea ohjelman tarvitseman laskutoimituksen tuloksen, muuttaa tietokoneen muistissa olevia tietoja jollain tarkoituksenmukaisella tavalla, tulostaa jotakin näytölle tai jonkin yhdistelmän edellisistä.
Ohjelmoija voi itse laatia uusia aliohjelmia ja käyttää niitä; hän voi myös käyttää toisten laatimia aliohjelmia. Kokonaisia ohjelmia rakennetaan aliohjelmia yhdistelemällä. Muun muassa GoodStuff-ohjelmassa on useita yhteen toimivia aliohjelmia, mutta niihin pääsemme käsiksi vasta toisella kierroksella. Tarkoitus on edetä seuraavasti:
Tämä luku käsittelee sitä, miten voit käyttää jo valmiiksi luotuja aliohjelmia. Tässä vaiheessa emme vielä välitä siitä, miten käyttämämme aliohjelmat on tehty.
Ensimmäisen kierroksen päättävissä luvuissa 1.7 ja 1.8 tutustut eräiden valmiiksi määriteltyjen aliohjelmien toteutukseen eli siihen, miten aliohjelmat on sisäisesti rakennettu ja miten ne toimivat ohjelmaa ajettaessa. Samalla pääset harjoittelemaan omienkin aliohjelmien laatimista.
Aliohjelmat ovat viimeinen niistä palasista, jotka tarvitsemme ryhtyäksemme opettelemaan laajemman ohjelmakokonaisuuden laatimista olio-ohjelmoinnin keinoin seuraavalla kierroksella.
Alkuvalmistelut
Tässä luvussa tarvitset uutta ohjelmamoduulia nimeltä Aliohjelmia, johon olemme koonneet sekalaisia aliohjelmia kokeiltaviksesi. Noudata luvusta 1.2 tuttuja vaiheita noutaaksesi moduulin IntelliJ’hin ja ottaaksesi sen sisältämät aliohjelmat käyttöön REPLissä. Tässä pikakertaus:
Nouda projekti IntelliJ’hin. A+ Courses-välilehdeltä IntelliJ’n oikeasta reunasta.
Käynnistä REPL Aliohjelmia-moduuliin. Valitse Aliohjelmia Project-näkymässä vasemmalla. Paina sitten Ctrl+Shift+D tai valikosta Tools → Scala REPL.
REPLin otsikkona pitäisi näkyä REPL for Aliohjelmia.
Aliohjelmaesimerkki
Ajatellaan tilannetta, jossa meillä on lukuja — vaikkapa mittaustuloksia — tallennettuna puskuriin. Esimerkiksi näin:
val tulokset = Buffer(-2, 0, 10, -100, 50, 100, 5, -5, 2)tulokset: Buffer[Int] = ArrayBuffer(-2, 0, 10, -100, 50, 100, 5, -5, 2)
Oletetaan vielä, että puskuriimme tallennetut negatiiviset luvut ovat kirjauksia epäonnistuneista mittauksista ja ne halutaan nyt poistaa puskurista.
On mahdollista määritellä aliohjelma, joka hoitaa negatiivisten lukujen poistamisen mistä
tahansa puskurista. Tällainen aliohjelma onkin valmiiksi määritelty Aliohjelmia-moduuliin.
Aliohjelman nimi on poistaNegatiiviset
ja toiminta-ajatus seuraava:
Kun aliohjelmaa käytetään, sille annetaan parametriksi viittaus johonkin sellaiseen puskuriin, joka sisältää kokonaislukuja.
Aliohjelma muokkaa tuota puskuria poistamalla siitä kaikki negatiiviset luvut.
Aliohjelman kutsuminen ja parametrit
Kun määräämme ohjelmakoodissa tietokoneen suorittamaan jonkin aliohjelman, sanotaan, että
kutsumme tuota aliohjelmaa (call, invoke, joskus myös apply). Kokeillaan nyt
kutsua poistaNegatiiviset
-aliohjelmaa. Se onnistuu helposti:
poistaNegatiiviset(tulokset)
Tässä tapauksessa parametrilausekkeita on vain yksi. Sen arvo on viittaus edellä luotuun puskuriin. Tämä viittaus välitetään aliohjelman käytettäväksi.
poistaNegatiiviset
-kutsun suorittavalla käskyllä ei itsellään ole arvoa (tai ainakaan
merkityksellistä arvoa; tästä lisää jäljempänä) samassa mielessä kuin vaikkapa lausekkeella
1 + 1
. Niinpä REPL vastaa tyhjää. Kuitenkin voimme katsoa muuttujan tulokset
arvon ja
todeta aliohjelman muuttaneen puskuria, johon tuo muuttuja viittaa:
tuloksetres0: Buffer[Int] = ArrayBuffer(0, 10, 50, 100, 5, 2)
println
ja kumppanit aliohjelmina
Oliko äskeisessä esimerkissä jotain tuttua? Juuri samaan tapaanhan olet oppinut käyttämään
esimerkiksi println
- ja play
-käskyjä: ensin käskyn nimi ja sulkeisiin parametrilauseke.
Kyseessä ei ole sattuma. Vaikka emme aiemmin kiinnittäneet asiaan huomiota, niin println
on aliohjelma juuri samassa mielessä kuin poistaNegatiiviset
. Tosin println
on
yleiskäyttöisempi ja määritelty osaksi Scala-kieltä, kun taas poistaNegatiiviset
on
kurssimateriaalin tätä lukua varten keksitty esimerkki. Aliohjelmia ovat vastaavasti myös
play
ja luvun 1.3 show
.
Katsotaan seuraavaksi aivan toisenlaista aliohjelmaa.
Arvon palauttava aliohjelma
keskiarvo
-aliohjelman idea on yksinkertainen: sille annetaan parametreiksi kaksi lukua,
ja se laskee niiden keskiarvon. Tämä aliohjelma palauttaa (return) arvon:
keskiarvo(5.0, 11.0)res2: Double = 8.0
keskiarvo
-aliohjelmalle kuuluu antaa kaksi parametria, joiden
on oltava lukuarvoisia. Parametrilausekkeet erotetaan pilkuilla.
keskiarvo
-aliohjelmakutsu on lauseke, jonka arvo on
aliohjelman laskema keskiarvo. Tätä arvoa sanotaan aliohjelman
paluuarvoksi (eli palautusarvoksi; return value).
REPL raportoi tämänkin lausekkeen arvon tutusti.
Alla on vielä muutama lisäesimerkki tämän aliohjelman kutsumisesta. Kuten esimerkeistä näkyy, aliohjelmakutsun muodostamaa lauseketta voi sitäkin käyttää isompien lausekkeiden osana:
val eka = keskiarvo(5, 1) + 2.1eka: Double = 5.1 val toka = keskiarvo(10.9, eka) + keskiarvo(-5, -10) - 1toka: Double = -0.5 1 + keskiarvo(toka + 1, 1)res1: Double = 1.75
Aliohjelmakutsun parametrilausekkeet voivat olla literaalien lisäksi myös vaikkapa muuttujien nimiä tai muita tietotyypiltään sopivia lausekkeita.
Ohjelman tila ja siihen vaikuttaminen
Katsotaan kohta lisää esimerkkejä aliohjelmista, mutta pysähdytään ensin pohtimaan jo nähtyjen aliohjelmien piirteitä.
Olet nähnyt tässä luvussa kaksi hyvin erilaista aliohjelmaa:
poistaNegatiiviset
-aliohjelman kutsut käskevät tietokonetta hoitamaan tietyn asian. Tällainen kutsu aiheuttaa muutoksia tilaan (state), jonka ohjelma on tallentanut tietokoneen muistiin. Voimme sanoa tällaista aliohjelmaa vaikutukselliseksi (effectful) aliohjelmaksi. Vaikutuksellinen aliohjelma ei välttämättä palauta mitään kiinnostavaa arvoa.keskiarvo
-kutsu ei muokkaa mitään muistiin tallennettua tietoa. Voimme sanoa tällaista aliohjelmaa vaikutuksettomaksi (effect-free) aliohjelmaksi. Jotta se olisi hyödyllinen, vaikutuksettoman aliohjelman on tuotettava merkityksellinen paluuarvo, minkäkeskiarvo
tekeekin.
Lisäksi olemme käyttäneet println
-aliohjelmaa, joka tulostaa merkkejä näytölle.
Kun println
-käsky suoritetaan, se vaikuttaa maailman tilaan siinä mielessä, että
ohjelman näytölle tuottama tuloste muuttuu havaittavasti. Myös println
kuuluu siis
vaikutuksellisten aliohjelmien joukkoon kuten poistaNegatiiviset
kin. Luvun 1.3
esittelemät play
ja show
ovat nekin vastaavasti vaikutuksellisia.
Saman luvun circle
ja rectangle
ovat myös aliohjelmia. Ne ovat vaikutuksettomia
samassa mielessä kuin keskiarvo
. Siinä missä keskiarvo vain laskee ja palauttaa
parametriensa perusteella lukuarvon, circle
ja rectangle
vain tuottavat
parametriensa perusteella kuvan (näyttämättä sitä ruudulla).
Kustakin aliohjelmasta voi pohtia: Muuttuuko jokin asia aliohjelman suorituksen seurauksena havaittavalla tavalla? (Ajan kulumista suorituksen aikana ja paluuarvon saamista ei lasketa muuttumiseksi.) Aliohjelmat voidaan jakaa vaikutuksellisiin ja vaikutuksettomiin tällä perusteella.
Jo tässä vaiheessa kurssia tämä jako osoittaa, että aliohjelmilla voi tehdä erilaisia asioita. Myöhemmin osoittautuu, että jaottelulla on laaja-alainen merkitys ohjelmoinnin kannalta. On esimerkiksi olemassa ohjelmointisuuntauksia, joissa perustellusti käytetään vain vaikutuksettomia aliohjelmia (luku 11.2). Ei mennä vielä siihen; voimme sen sijaan jo nyt hyödyntää tätä jaottelua selkiyttämään aliohjelmiin liittyviä termejä ja käsitteitä.
Tärkeä termi: funktio
Useiden ohjelmointikielten yhteydessä kaikkia aliohjelmia on tapana sanoa funktioiksi (function), niin vaikutuksellisia kuin vaikutuksettomiakin. Näin on esimerkiksi Scala-kielen tapauksessa. Jatkossa puhummekin usein funktioista, ja tällä kurssilla funktio on siis sama asia kuin aliohjelma.
"Funktio" kuulostaa sanana tutulta, ja syystäkin, mutta:
Varo matikkaa! (jälleen kerran)
Moni ohjelmoinnin aloittelija on kompastellut funktion käsitteeseen tuon termin matematiikkayhteyden takia.
On totta, että vaikutuksettomat funktiot muistuttavat koulumatematiikan funktioita: otetaan sisään parametriarvoja ja tuotetaan jokin tulos (paluuarvo), ja siinä kaikki. Kun vain selvitetään, mikä on matemaattisen funktion f(x) arvo tietylle x:lle, ei muutu mitään tallennettua tietoa tai tulostu mitään näytölle.
Kuitenkin kun Scala-funktiota kutsutaan, tulostuksia tai muita vaikutuksia ohjelman tilaan voi tapahtua. On tärkeää huomata, että esimerkiksi Scala-ohjelmoinnissa funktion määritelmä kattaa myös vaikutukselliset aliohjelmat. Pidä tämä mielessä, kun jatkossa puhutaan funktioista!
Myös vaikutuksettomilla funktioilla on tietokoneohjelmoinnissa oleellinen ero tutunlaisiin matemaattisiin funktioihin verrattuna: ne eivät ainoastaan kuvaa riippuvuussuhteita kuten f(x) = x + 1, vaan myös niitä vaiheittaisia prosesseja (algoritmeja), joilla paluuarvot saadaan laskettua. Tämä seikka konkretisoituu seuraavassa luvussa.
Erilaisista funktioista kootusti
Tässä taulukko mainituista funktiotyypeistä. Eräät kohdista on alleviivattu; vie hiiren kursori niiden päälle, niin saat lisäselityksiä.
Vaikuttaako funktio tilaan? |
Palauttaako arvon? |
Kurssilla käytetty termi |
Muita termejä |
---|---|---|---|
Ei vaikuta koskaan |
Palauttaa |
Vaikutukseton funktio |
Sivuvaikutukseton funktio |
Ei palauta |
Tyhjä funktio |
||
Vaikuttaa ainakin joskus |
Palauttaa |
Vaikutuksellinen
funktio
|
Sivuvaikutuksellinen funktio, proseduuri |
Ei palauta |
Kirjastofunktioita pakkauksesta scala.math
Scala-kielen mukana tulee pakkaus nimeltä scala.math
, joka sisältää funktioita yleisiin
matemaattisiin tarpeisiin.
Esimerkiksi funktio sqrt
laskee ja palauttaa neliöjuuren (square root):
scala.math.sqrt(100)res2: Double = 10.0 scala.math.sqrt(25)res3: Double = 5.0
Kutsumme tässä funktiota sen täydellisellä nimellä, jossa yhdistyvät
pakkauksen nimi scala.math
ja funktion varsinainen nimi sqrt
.
(Alempana kerrotaan, miten tämä käy kätevämmin.)
Tämä vaikutukseton funktio ei muuta tilaa. Se vain palauttaa tuloksen.
Saman pakkauksen funktiot max
ja min
ovat monissa ohjelmissa käteviä. Nämä
vaikutuksettomat funktiot palauttavat kahdesta parametristaan suuremman ja pienemmän:
scala.math.max(2, 10)res4: Int = 10 scala.math.max(15, -20)res5: Int = 15 scala.math.min(2, 10)res6: Int = 2 scala.math.min(15, -20)res7: Int = -20
Luvussa 1.1 jo mainittiinkin, että ohjelman rakennuspalikoita, jotka ovat tarjolla toisten ohjelmoijien käyttöä varten, sanotaan usein kirjastoksi (library). Kirjaston sisältämiä funktioita — kuten äskeisiä — sanotaan vastaavasti kirjastofunktioiksi.
Saimme nyt kutsuttua noita kirjastofunktioita, mutta oli melko kurjaa kirjoittaa tuo
pakkauksen nimi scala.math
jokaiseen funktiokutsuun. Tähän monisanaisuuteen löytyy
lääke. Perehdytään siihen tässä välissä ja tutkitaan sitten lisää funktioita.
Pakkauksista ja import
-käskyistä
Koodi on tarjolla pakkauksissa
Kurssilla on jo mainittu muutamia koodipakkauksia. Esimerkiksi:
Äsken käytimme funktiota pakkauksesta
scala.math
. (Tuo pakkaus tulee Scala-työkaluston mukana.)Kurssityökalut kuten
play
,Color
jaPic
sisältää pakkaus nimeltäo1
. (Sen koodi löytyy IntelliJ’ssä O1Library-moduulista.)Luvussa 1.5 mainittiin
Buffer
-tyypin määrittelevä pakkausscala.collection.mutable
. (Tämäkin pakkaus tulee Scala-työkalustossa.)
Työkalujen jaottelu eri pakkauksiin on tarpeen muun muassa siksi, että isompiin
ohjelmiin voi helpostikin tulla keskenään samannimisiä osia esimerkiksi eri ohjelmoijien
laatimina. Nimet kuten play
, show
tai min
voivat olla toisissa ohjelmissa
muunmerkityksisiä kuin esimerkeissämme.
Ongelma ratkeaa sijoittamalla keskenään samannimiset osat eri pakkauksiin, jolloin pakkauksen nimellä voi ilmaista, minkä pakkauksen sisällöstä on kyse. Haittapuolena on, että kun tarvitsemme työkaluja jostakin pakkauksesta, on koneelle tavalla tai toisella ohjeistettava, että käytämme juuri tuon pakkauksen sisältöä. Pakkauksen nimen toistuva kirjoittaminen on ärsyttävää ja voi vaikeuttaa koodin lukemista.
Kätevämpää pakkausten käyttöä: import
-käsky
Voimme ilmoittaa etukäteen, että aiomme myöhemmässä koodissa käyttää tietyn pakkauksen sisältöä:
import scala.math.sqrt
Käytännössä tuo import
-käsky tarkoittaa: "Aina, kun jäljempänä sanotaan sqrt
, niin se
tarkoittaa juuri scala.math
-pakkauksen määrittelemää sqrt
tä."
import
-käsky ei ole lauseke, eikä sillä ole varsinaista arvoa. REPL ei ihmeemmin kuittaa
käskyä tulosteella, mutta vastaanottaa sen kyllä.
Näin ilmoitettuamme voimme kutsua tuota funktiota lyhyttä nimeä sqrt
käyttäen:
sqrt(100)res8: Double = 10.0 sqrt(25)res9: Double = 5.0
Ellei import
-käskyä olisi ensin annettu, olisimme saaneet tällaisen virheilmoituksen:
sqrt(100)-- Error: |sqrt(100) |^^^^ |Not found: sqrt
Jos saat ohjelmoidessasi tuollaisen ilmoituksen, tarkista ensin oikeinkirjoitus ja
toiseksi, että olet muistanut antaa import
-käskyn.
Koko pakkaus käyttöön tähtimerkillä
Usein halutaan käyttää samasta pakkauksesta monia eri työkaluja. Saatamme vaikkapa haluta
käyttää scala.math
-pakkauksesta useaa eri funktiota.
Yksi mahdollisuus on import
ata kukin funktio erikseen, mutta usein kätevämpää on ottaa
pakkauksen koko sisältö käyttöön kerralla:
import scala.math.*
Tähtimerkin *
voi tuossa ajatella tarkoittavan "kaikki".
Nyt voimme käyttää mitä vain tuon pakkauksen funktioista lyhyin merkinnöin:
sqrt(100)res10: Double = 10.0 max(2, 10)res11: Int = 10 min(2, 10)res12: Int = 2
Kysyttyä: kuluuko enemmän muistia, jos käytän tähteä import
-käskyssä?
Ei. import
illa ainoastaan kirjataan, mitä tarkoitetaan, kun
käytetään tiettyjä nimiä kyseisessä ohjelmakoodissa. Esimerkiksi
import scala.math.*
määrittää, että kun käytetään mitä
tahansa nimistä min
, max
, sqrt
jne., tarkoitetaan juuri
scala.math
-pakkauksen osia.
On totta, että tietokoneen on pidettävä muistissa ohjelman käyttämät työkalut (funktiot yms.). Mutta ohjelman ajonaikaisen muistinkäytön kannalta oleellista on nimenomaan se, mitä työkaluja todella käytetään, eikä se, millaisella ilmaisulla niitä on ilmoitettu käytettävän.
Milloin import
-käskyä ei tarvita?
Tarvitsimme siis import
-käskyä käyttääksemme scala.math
-pakkausta kätevästi. Myöhemmin
käytämme sitä myös muihin pakkauksiin. Silti tähän saakka pärjäsimme hyvin ilmankin.
Olemme käyttäneet esimerkiksi funktioita println
, play
ja show
sekä tyyppejä Int
,
String
ja Buffer
ilman import
-käskyä; ne ovat "toimineet suoraan". Kuitenkin nekin
sijaitsevat eräissä pakkauksissa. Miksei niitä tarvinnut import
ata?
Ensinnäkin: import
-käskyä ei tarvitse antaa, jos kyseessä on tietty, Scala-kieleen
aivan erottamattomasti liittyvä pakkaus, jonka nimi on yksinkertaisesti scala
.
Tämä erikoispakkaus määrittelee mm. tyypit Int
, Double
ja String
; esimerkiksi
Int
-tietotyypin täydellinen nimi on scala.Int
. Tuon pakkauksen sisältö on aina
käytettävissä kaikissa Scala-ohjelmissa.
Toiseksi: käyttämämme REPL-ympäristö valitsee käyttöösi eräitä pakkauksia automaattisesti,
jotta et joudu naputtelemaan sinne joka session alussa samoina toistuvia import
-käskyjä.
Tarkemmin sanoen: kun käynnistät REPLin johonkin kurssimoduuliin, se import
aa
automaattisesti seuraavasti:
Saat käyttöösi pakkauksen
o1
aivan kuin olisit itse aluksi kirjoittanutimport o1.*
. Tämän vuoksi esimerkiksiplay
jaPic
toimivat REPLissä helposti. Joissakin moduuleissa REPLimport
aa automaattisesti myös muita moduulin sisältämiä pakkauksia (mikä näkyy silloin REPLin tervehdystulosteessa). Esimerkiksi kun käynnistit REPLin Aliohjelmia-moduuliin, REPL poimi käyttöön pakkausteno1
jao1.aliohjelmia
sisällön.Saat käyttöösi
Buffer
-kokoelmat pakkauksestascala.collection.mutable
aivan kuin olisit itse aluksiimport
annut ne. Käytämme puskureita kurssin alun REPL-esimerkeissä usein, joten halusimme säästää sormiasi hieman.
Jos annat turhan import
-käskyn, niin ei siitä mitään kauheaa tapahdu. Jos taas unohdat
tarvittavan import
in, saat not found -virheilmoituksen yllä kuvattuun tapaan ja voit
korjata asian.
Kysyttyä: import
ja nimikonfliktit
Mitä tapahtuu jos import
taa kaksi pakkausta,
joissa on kaksi samannimistä funktiota, jonka
jälkeen käyttää sitä funktiota ilman pakkauksen
nimeä? Kumman Scala valitsee, vai valitseeko
kumpaakaan?
Moniselitteisestä nimestä tulee virheilmoitus. Tuollaiset nimikonfliktit eivät ole suuri harvinaisuus varsinkaan silloin, jos ohjelmasi yhdistelee eri ohjelmoijien laatimia kirjastoja.
Osan nimikonflikteista voi kiertää jättämällä tähtimerkin
import
-käskystä ja valitsemalla käyttöön vain ne osat kustakin
pakkauksesta, joita todella tarvitset.
Jos tarvitset samassa ohjelmassa kahta samannimistä funktiota eri pakkauksista, voit aina käyttää funktioiden täydellisiä nimiä, joissa on mukana pakkauksen nimi pisteineen alussa:
scala.math.sqrt(30)res13: Double = 5.477225575051661 my.imaginary.tools.rounding.sqrt(30)res14: Int = 5
Toinen vaihtoehto on ottaa ainakin toinen funktioista käyttöön toisella nimellä. Se käy tähän tapaan:
import scala.math.{sqrt as juuri}juuri(100)res15: Double = 10.0
Sitten lisää funktioita.
Funktiokavalkadi
Seuraavaksi tutustumme esimerkin vuoksi koko joukkoon eri funktioita. Näin saat käsitystä siitä, mitä funktiot voivat tehdä, ja pääset harjoittelemaan funktiokutsuja sisältävän ohjelmakoodin lukemista.
Käytämme sekä eräitä Scalan kirjastofunktioita, jotka ovat hyvin yleiskäyttöisiä, että eräitä kurssia varten laadittuja esimerkkifunktioita.
Lisää matemaattisia kirjastofunktioita
Yllä esiteltiin scala.math
-pakkauksen funktiot sqrt
, max
ja min
. Samassa
pakkauksessa on muitakin funktioita, joilla voi määrätä suoritettavaksi erilaisia
laskutoimituksia.
Alla on esimerkkejä funktioista abs
(absolute value: itseisarvo), pow
(power:
potenssiin korotus) ja sin
(sine: sinifunktio). Nämä funktiot on otettava käyttöön
joko yksittäin tai kaikki kerralla kuten tässä:
import scala.math.*abs(-50)res16: Int = 50 pow(10, 3)res17: Double = 1000.0 sin(1)res18: Double = 0.8414709848078965
Kaikki nämä funktiot ovat vaikutuksettomia. Ne vain palauttavat arvoja eivätkä muuta tallennettua dataa tai tulosta tai soita tai piirrä mitään.
Kokeile ainakin joitakin esitellyistä funktioista itse REPLissä. Voit kokeilla myös
näitä: muut trigonometriset funktiot (cos
, atan
jne.), cbrt
(kuutiojuuri), hypot
(hypotenuusa; parametreiksi kaksi kateetinmittaa), floor
(alaspäin pyöristys), ceil
(ylöspäin pyöristys), round
(lähimpään pyöristys), log
ja log10
(logaritmeja).
Koko luettelo löytyy Scalan dokumentaatiosta,
joka tosin ei ole kaikilta osin aloittelijaystävällinen.
Tarkoituksena ei nyt missään nimessä ole opetella mainittuja kirjastofunktioiden nimiä
ulkoa, vaikka ne voivatkin myöhemmin tulla tarpeeseen. Nimet voi tarvitessa tarkistaa
dokumentaatiosta tai täältä oppimateriaalista. Jos ja kun jokin funktio on niin
yleishyödyllinen, että sitä tulee tarvittua usein, niin nimen oppii ulkoa vahingossakin.
(Vinkkinä kuitenkin, että erityisesti max
- ja min
-funktioita käytetään tulevissa
luvuissa runsaasti.)
Funktioiden käyttö: esimerkkejä ja tehtäviä
Funktio voi hyödyntää ulkoisia resursseja kuten tiedostoja tai nettilähteitä. Seuraava kurssia varten laadittu esimerkkifunktio selvittää, mikä on IMDb-leffasivustolla äänestettyjen Top 250 -elokuvien listalla tietyllä sijaluvulla:
imdbLeffa(3)res19: String = The Godfather: Part II imdbLeffa(1)res20: String = The Shawshank Redemption
Esimerkkifunktio toimii ilman nettiyhteyttäkin, koska se on laadittu ottamaan tietonsa
Aliohjelmia-moduulin mukana tulevasta kansiosta top_movies
. Leffaluettelo ei siis
ole aivan ajantasainen.
(Muistutus: tuo toimii, kunhan olet käynnistänyt REPLin juuri Aliohjelmia-moduuliin.)
Vielä yksi esimerkki
On myös mahdollista laatia funktio, joka vuorovaikuttaa ohjelman käyttäjän kanssa, kun
sitä kutsutaan. Tästä esimerkkinä on laadittu käyttöösi funktio nimeltä pelaaPylpyrapelia
.
Se haastaa sinut yksinkertaiseen peliin, jossa kone pääsee pätemään. Voit kokeilla
sitä omin päin. Funktiolle tulee antaa parametriksi pelaajan nimi merkkijonona.
(Jos funktion kutsuminen näyttää vain jättävän REPLin jumiin, on voinut käydä niin, että
pieni peli-ikkuna on jäänyt muiden ikkunoiden taakse. Löytänet sen pienentämällä ainakin
IntelliJ-ikkunan.)
Funktiokutsuja sisäkkäin
Funktiokutsuun merkitään parametreiksi sopivantyyppisiä lausekkeita. Funktiokutsu itsekin on lauseke. Niinpä funktiokutsuja voi kirjoittaa sisäkkäin:
Jos sisäkkäisiä funktiokutsuja on paljon, voi olla selkeämpää muotoilla koodi toisin. Muuttujat sopivat tähän. Animaatioesimerkin viimeisen rivin voisi korvata vaikkapa näillä käskyillä:
val potenssi = pow(2, 5)potenssi: Double = 32.0 val pienempi = min(potenssi, 100 - sqrt(100))pienempi: Double = 32.0 println(abs(-5.5) + pienempi)37.5
Tai vaikka näinkin:
val potenssi = pow(2, 5)potenssi: Double = 32.0 val erotus = 100 - sqrt(100)erotus: Double = 90.0 val pienempi = min(potenssi, erotus)pienempi: Double = 32.0 val lopputulos = abs(-5.5) + pienempilopputulos: Double = 37.5 println(lopputulos)37.5
Käytä tässä tyyliasiassa maalaisjärkeä. Mieti tapauskohtaisesti, mikä on itsellesi selkein ja luontevin tapa. Kaikkiin esitettyihin tyyleihin on joka tapauksessa syytä totutella, vaikka niitä et kaikkia itse käyttäisikään, sillä ohjelmointiin (ja ohjelmoinnin opiskeluun) sisältyy myös paljon toisten kirjoittaman koodin lukemista.
Pikkutehtävä: lausekkeen evaluointijärjestys
Unit
-arvo
Scalassa (ja useassa muussa kielessä) on erityislaatuinen arvo nimeltä Unit
. Tätä arvoa
käytetään sellaisten funktioiden paluuarvona, jotka eivät palauta mitään merkityksellistä
arvoa. Tällaisia funktioitahan ovat esimerkiksi println
, play
ja yllä kuvailtu
poistaNegatiiviset
. Teknisesti ottaen nämäkin funktiot palauttavat nimittäin arvon —
Unit
-arvon.
Unit
-paluuarvolla ei voi tehdä käytännöllisesti katsoen mitään. Voit ajatella,
että se vain tarkoittaa: "funktion suoritus päättyi, mutta mitään oleellista ei syntynyt
paluuarvona". Esimerkiksi poistaNegatiiviset
-funktio vain muuttaa puskurin tilaa
eikä tuota mitään "tulosta". Käytännössä voimme tällä kurssilla sanoa, että Unit
tarkoittaa "ei mitään paluuarvoa" ja että esimerkiksi println
- ja poistaNegatiiviset
-funktiot "eivät palauta arvoa".
Vaikka Unit
-arvo ei Scala-ohjelmakoodissa välttämättä kovin usein näykään, se ei ole
aloittelijallekaan pelkkä kuriositeetti. Sen olemassaolosta on hyvä tietää jo vaikka
siksikin, että sana esiintyy joskus virheilmoituksissa. Esimerkiksi yritys laskea
1 + println("kissa")
tuottaa virheilmoituksen, jossa valitetaan sitä, ettei
ykköstä voi laskea Unit
in kanssa yhteen. Lisäksi Unit
-sanaa käytetään runsaasti
Scala-ohjelmien kuvauksissa (dokumentaatiossa; luku 3.2), kun halutaan ilmoittaa, ettei
tietyllä ohjelman osalla ole merkityksellistä paluuarvoa.
Unit
kurssimateriaalin animaatioissa
Aiemmissa kurssimateriaaliin upotetuissa animaatioissa Unit
-paluuarvoja ei ole
näkynyt. Tulevissa ne kuitenkin näkyvät kuten seuraavassa pikkuanimaatiossa.
println
-funktiokutsukin palauttaa arvon, joskin sisällöttömän sellaisen:
Palauttaminen vs. tulostaminen
Aloitteleville ohjelmoijille tuottaa joskus vaikeuksia hahmottaa arvon tulostamisen ja arvon palauttamisen välistä eroa. REPLin käyttö ei välttämättä selkiytä tilannetta. Korostetaan siis vielä tätä eroa:
Palauttaminen on yleinen funktioiden piirre. Funktio voi palauttaa arvon "vastauksena" sille taholle, joka funktiota kutsuu.
Paluuarvo ei välttämättä tule mihinkään näkyviin.
Funktiota kutsunut ohjelmakoodi voi tehdä arvolla mitä vain, esimerkiksi käyttää paluuarvoa laskutoimituksen osana, tulostaa sen, välittää sen
play
-funktiolle tai vain jättää sen käyttämättä.Pelkkä palauttaminen ei vaikuta ohjelman tilaan edellä kuvaillussa mielessä.
Tulostaminen tarkoittaa tekstin laittamista näkyviin esimerkiksi tietokoneen näytölle. Tämä onnistuu Scalassa
println
-käskyllä.Tulostettavan tekstin voi määrätä minkä vain lausekkeen arvo, esim.
println("Moi")
taiprintln(1 + 1)
.Lausekkeena voi olla myös funktiokutsu, vaikkapa
println(keskiarvo(10, 20))
, jolloin tulostetaan funktiota kutsumalla saatu paluuarvo.Tulostamisen luemme tilaan vaikuttamiseksi.
Palauttaminen ja tulostaminen REPLissä
REPL evaluoi sinne kirjoitetut lausekkeet ja tulostaa automaattisesti kuvaukset niiden
arvoista. Funktiokutsulausekkeen tapauksessa tämä tarkoittaa, että suoritetaan funktio
ja tulostetaan kuvaus sen palauttamasta arvosta. Esimerkiksi kun REPLiin syötetään
lauseke max(3 + min(1, 4), 3)
, niin tulostuu kuvaus max
-funktion paluuarvosta.
Kuitenkaan esimerkiksi min
-funktion paluuarvoa 1, jota käytettiin koko lausekkeen
arvon määrittämiseen, ei tulosteta.
Kun REPLissä kutsutaan funktiota, joka palauttaa Unit
-arvon, niin REPL ei tulosta tätä
sisällötöntä paluuarvoa.
Palauttaminen ja tulostaminen samassa funktiossa
Edellä jo näkyi, että println
paitsi tulostaa myös palauttaa arvon, joskin vain Unit
-arvon. Voidaan kyllä määritellä sellainenkin funktio, joka sekä tulostaa näkyviin jotain
että tuottaa varsinaisen paluuarvon.
Funktioista ja abstraktioista
Funktion laatiminen työkaluksi
Jos tavoitteenamme on saada laskettua vaikkapa jonkin luvun sini tai neliöjuuri, niin
valmis työkalu löytyy kirjastofunktiona. Kun valmista ratkaisua ei löydy, muodostamme oman
ratkaisun olemassa olevia työkaluja yhdistelemällä. Jos esimerkiksi haluamme saada selville,
paljonko on sellaisen pallon tilavuus,
jonka säde on 6371 km, niin voimme hyödyntää aritmeettisia operaattoreita ja pow
-potenssiinkorotusfunktiota:
import scala.math.pow4 * 3.14159 * pow(6371, 3) / 3res21: Double = 1.0832060019000126E12
Tuossa vain evaluoimme tiettyjä arvoja sisältävän lausekkeen, mistä saimme yhden halutun arvon tulokseksi. Jos kuitenkin haluamme pystyä käsittelemään erilaisia tapauksia — siis laskemaan erikokoisten pallojen tilavuuksia — on kätevämpää rakentaa oma työkalu juuri tähän tarkoitukseen. Se voisi toimia vaikkapa näin:
pallonTilavuus(6371)res22: Double = 1.0832060019000126E12 pallonTilavuus(1)res23: Double = 4.188786666666666
Funktioiden hyötyjä
Yksi käytännön syy funktioiden määrittelemiselle tuli juuri esille. Kun tietty kenties monimutkainenkin toiminto on kerran toteutettu funktioksi, voi toimintoa käyttää helposti funktiota kutsumalla. Pallon uudelleen keksimiseltä vältytään siinäkin mielessä, että toistuvasti tarvittu toiminto voidaan toteuttaa funktioksi monen ohjelmoijan käyttöön.
Lisäksi funktiot auttavat jaottelemaan ohjelmakoodin selkeärajaisiin, nimettyihin osakokonaisuuksiin. Tämä parantaa ohjelmakoodin luettavuutta ja muokattavuutta.
Kolmannenlainen funktioiden tuoma hyöty on, että funktion sisään voi piilottaa jonkin
sellaisen toiminnon toteutuksen, jonka yksityiskohtia funktion käyttäjän ei tarvitse
tuntea. Jos pallonTilavuus
-funktio on määritelty, sitä voi käyttää sellainenkin, joka
ei tunne kyseistä laskentakaavaa. Olet itse tässä luvussa käyttänyt erilaisia sisäisesti
melko monimutkaisiakin funktioita, vaikka et opetetun perusteella osaisi niitä itse
toteuttaa tai niiden toteutusta ymmärtää. On riittänyt, että tunnet tietyt piirteet
funktiosta: mitä parametreja se ottaa, mitä vaikutuksia (jos mitään) se aiheuttaa ja
mitä se palauttaa.
Funktiot ovat siis eräs abstrahoinnin (abstraction) muoto.
Abstraktiot ohjelmoinnissa
abstract: something that concentrates in itself the essential qualitiesof anything more extensive or more general; essence—yksi sanan "abstract" määritelmistä Infoplease.com-sivustolla
Abstrahoimalla voimme jättää välittämättä yksityiskohdista tai yksittäisistä tapauksista.
Esimerkiksi funktio pallonTilavuus
kuvaa yleisellä tasolla sitä kaavaa (algoritmia),
jolla pallon tilavuuden voi eri tapauksissa laskea.
Olemme jo kohdanneet muitakin abstraktion muotoja kuin funktiot. Esimerkiksi muuttuja on
abstraktio: lausekkeessa luku + 1
kuvaamme muuttujan nimellä yleisesti, että haluamme
tehdä tietyn laskutoimituksen, kuitenkin ottamatta tässä kantaa siihen yksityiskohtaan,
mikä luku
-muuttujan arvo on kyseisessä tapauksessa.
Parametrit tekevät funktiosta abstraktimpia. Koska sin
-funktio ottaa parametrin (esim.
sin(1)
), se on voitu laatia välittämättä niistä nimenomaisista tilanteista, joissa
funktiota eri ohjelmissa käytetään. Niinpä se on abstraktimpi ja yleishyödyllisempi kuin
muuttuja luvunYksiSini
olisi.
Voi sanoa, että ohjelmointi on pitkälti hyödyllisten abstraktioiden suunnittelemista, toteuttamista ja käyttämistä. Tähän teemaan palaamme useasti kurssin mittaan (esim. luvuissa 2.1 ja 3.2). Omien funktioiden toteuttamista pääset harjoittelemaan heti seuraavassa luvussa.
Yhteenvetoa
Aliohjelma on toteutus tietylle toiminnolle. Esimerkiksi Scalan yhteydessä aliohjelmia kutsutaan funktioiksi.
Funktio voi vaikuttaa ohjelman tilaan tai palauttaa arvon (esim. laskutoimituksen tuloksen) tai tehdä molemmat näistä.
Käskyä suorittaa funktio sanotaan funktion kutsumiseksi. Funktiolle voi välittää parametreja sitä kutsuessa.
Ohjelmointikielten yhteyteen on määritelty ns. kirjastoja; esimerkiksi Scala-kieleen liittyy yleisiä matemaattisia kirjastofunktioita.
Funktiot ovat eräs ohjelmoinnissa käytetty abstraktion muoto. Abstraktiot auttavat ohjelmoijaa työstämään monimutkaisiakin kokonaisuuksia ja vähentävät turhaa työtä.
Unit
on erikoisarvo, joka merkitsee vain "ei merkityksellistä (palautus)arvoa".import
-käsky on kätevä, kun haluat käyttää jonkin pakkauksen sisältämää työkalustoa.REPL-ympäristömme hoitaa kaikkein tyypillisimmät
import
-käskyt automaattisesti, mutta REPLissäkin sinun on itse joskusimport
attava pakkauksia (kutenscala.math
) käyttöön.
Lukuun liittyviä termejä sanastosivulla: funktio / aliohjelma; funktiokutsu, parametrilauseke, paluuarvo; vaikutukseton funktio, vaikutuksellinen funktio; yksikkötyyppi eli
Unit
; kirjasto; abstraktio.
Edellisen luvun käsitekaavio funktioilla höystettynä:
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, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Joel Toppinen, Anna Valldeoriola Cardó ja Aleksi Vartiainen.
Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.
Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista suunnittelivat Juha Sorva ja Teemu Sirkiä. Teemu Sirkiä ja Riku Autio toteuttivat ne apunaan Teemun aiemmin rakentamat työkalut Jsvee ja Kelmu.
Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset laati Juha Sorva.
O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen, Juha Sorva ja Jaakko Nakaza. 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; sitä ovat kehittäneet kymmenet Aallon opiskelijat ja muut.
A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen suunnitteluun ja toteutukseen on osallistunut useita opiskelijoita yhteistyössä O1-kurssin opettajien kanssa.
Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.
Lisäkiitokset tähän lukuun
Luvussa tehdään vääryyttä Neal Heftin, Mike Oldfieldin ja Dave Stewartin säveltämälle musiikille. Kiitos ja anteeksi.
Kiitos Alex Purovedelle parannusehdotuksesta Pylpyräättöriin.
Aliohjelman kutsumiskäsky koostuu aliohjelman nimestä ja sulkeiden sisäisestä parametrilausekkeesta.