- CS-A1110
- Kierros 11
- Luku 11.2: Ohjelmointiparadigmoista
Luku 11.2: Ohjelmointiparadigmoista
Johdanto
Kauan sitten kakkoskierroksella tuli jo esille, että on olemassa erilaisia tapoja ohjelmoida — ohjelmointiparadigmoja — joista olio-ohjelmointi on yksi. Nyt kun kokemusta ohjelmoinnista on ainakin vähän karttunut, on hyvä aika palata aiheeseen.
Tärkeimpien paradigmojen tunteminen kuuluu ohjelmoijan yleissivistykseen. Aloittelijakin hyötyy siitä käytännössä: tutustuessasi erilaisiin ohjelmointilähteisiin törmäät pian mainintoihin ja keskusteluihin näistä paradigmoista.
Uskonsodista
Sana "paradigma" kielii perustavanlaatuisista ajattelutavoista, joihin liittyy joskus isoja tunteita. Tämän luvun aiheista on esitetty, ja esitetään edelleen, toisistaan rajusti poikkeavia näkemyksiä, kun eri paradigmojen kannattajat merkitsevät reviireitään.
Tämä luku yrittää esittää asiansa kiihkottomasti. Toivomme, että suhtaudut kaikkiin esille tuleviin paradigmoihin avoimesti — myös niihin, jotka aluksi tuntuvat sinusta oudommilta ja ehkä järjettömiltäkin. Muodosta mielipiteesi harkiten ohjelmointikokemuksesi vähitellen kasvaessa. Aiheesta taitetaan peistä osin siksi, että järkeviä argumentteja on suuntaan jos toiseenkin. Tosin käytetty todistusaineisto perustuu harmillisen usein vain henkilökohtaisiin vaikutelmiin.
Voi hyvin olla, että tulevaisuudessa koko nykyinen paradigmajaottelu muuttuu tarpeettomaksi, mutta ainakin toistaiseksi se on hyvä tuntea. Jos ei muuten niin siksi, että se vaikuttaa niin voimakkaasti siihen, miten ohjelmoinnista puhutaan ja kirjoitetaan.
Keskeisiä ohjelmointiparadigmoja
Ennen kuin katsomme minkään paradigman piirteitä tarkemmin, tarkastellaan muutaman keskeisen ohjelmointiparadigman välisiä suhteita. Niitä kuvaa seuraava lyhyt esitys, joka toimii samalla karttana lopulle tästä luvusta:
Tässä luvussa käsitellään vain kourallista paradigmoja. Niitä on muitakin; katso esimerkiksi Wikipedian artikkeli aiheesta.
Paradigman tunnistamisesta
Olisi helppoa, jos ohjelmia ja ohjelmointikieliä voisi lokeroida selkeästi kunkin yhteen paradigmaan. Näin ei kuitenkaan ole, ei lähimainkaan. Paradigmat ovat osin päällekkäisiä keskenään. Useimmat paradigmat ovat myös jonkin toisen "aliparadigmoja" eli sisältyvät johonkin laajempaan suuntaukseen. Jo siksikin tietty ohjelma voi edustaa useaa eri paradigmaa — ja yleensä edustaakin. Lisäksi toisistaan erillisiäkin paradigmoja voi yhdistellä laatimalla ohjelmakokonaisuuden osia eri tavoilla.
Ei yleensä ole järkevää etsiä kyllä tai ei -vastausta kysymykseen, edustaako tietty ohjelma tiettyä paradigmaa vai ei. Hedelmällisempää saattaa olla pohtia sitä, missä määrin jokin ohjelma edustaa tiettyä paradigmaa.
Ohjelmointikielikään ei yksiselitteisesti määrää ohjelmointiparadigmaa. Esimerkiksi Scala on suunniteltu kieleksi, jolla voi ohjelmoida eri paradigmojen mukaisesti ja paradigmoja yhdistellen. Tällä kurssilla laaditaan olio-ohjelmia, joista monet ovat selvästi imperatiivisia ja toiset enimmäkseen tai jopa kokonaan funktionaalisia.
Miten paradigmat sitten eroavat toisistaan? Vastaus on monimutkainen. Asiaa ei helpota sekään, että paradigmoilla ei ole kaikkien yleisesti hyväksymiä määritelmiä; tässä luvussa nojaudumme erääseen tulkintaan. Kuitenkin tarkastelemalla kahta keskeistä eroavaisuutta tulemme jo paljon viisaammiksi siitä, mitä paradigmakaavio yllä tarkoittaa. Tutkikaamme seuraavia kahta kysymystä:
Miten olio-ohjelmointi eroaa muusta ohjelmoinnista?
Miten imperatiivinen ohjelmointi eroaa funktionaalisesta ohjelmoinnista?
Olio-ohjelmointi vs. olioton ohjelmointi
Keskeisimpiä eroja
Voimme kuvailla olio-ohjelmoinnin perusolemuksen vertailemalla:
Olio-ohjelmointi |
Olioton, esim. proseduraalinen, ohjelmointi |
---|---|
Asioita kuvataan olioina ja toimintoja olioihin liittyvinä metodeina. |
Ohjelmaan kuuluvia toimintoja kuvataan funktioina (jotka eivät liity olioihin; olioita ei ole, tai jos onkin, niin niillä ei ole ratkaisevaa merkitystä). |
Ohjelman suoritus mielletään olioiden väliseksi viestinnäksi, jossa ne kutsuvat toistensa metodeita. Olio voi delegoida osatehtäviä toisille olioille. |
Ohjelman suoritus muodostuu "pääfunktion" kutsuessa "alifunktioita", jotka edelleen kutsuvat tarpeen mukaan omia "alifunktioitaan". |
Oliolla voi olla tallessa tietoja, joita se tarvitsee metodeita suorittaessaan. Metodikutsuihin liittyviä lisätietoja voidaan välittää sille parametreiksi. Olio voi myös kysyä tarvittavia tietoja toisilta olioilta. |
Funktioiden tarvitsemat tiedot välitetään niille parametreiksi. |
Voit yrittää kuvitella, millaista ohjelmointia nyt harjoittaisimme, jos olisimme kurssin ensimmäisen viikon jälkeen jättäneet olioiden, luokkien ja metodien käsitteet kokonaan väliin ja ruvenneet rakentamaan ohjelmia vain toisiaan kutsuvista funktioista. Tällöin olisimme päätyneet tekemään imperatiivista tai funktionaalista ohjelmointia oliottomasti.
Muita eroja ja piirteitä
Seuraavassa taulukossa on lueteltu eräitä piirteitä, jotka eivät määrittele olioparadigmaa samalla tavoin kuin yllä luetellut mutta jotka usein tähän paradigmaan liitetään. Toisessa sarakkeessa ovat jälleen "oliottomat" verrokit.
Olio-ohjelmointi |
Olioton, esim. proseduraalinen, ohjelmointi |
---|---|
Käytetään (useimmiten) luokkia kuvaamaan tiedon tyyppejä. |
Ei luokan käsitettä tai luokkia ja olioita käytetään vain alkeellisesti. |
Ohjelmoijan keskeinen tehtävä on tunnistaa ongelmakentän kuvaamiseksi tarvittavat käsitteet (oliot/luokat) ja niille metodit. |
Ohjelmoijan keskeisiin tehtäviin kuuluu tunnistaa ongelmakenttään liittyvät toiminnot (funktiot) sekä toimintojen tarvitsema data. |
Ohjelmat ja niiden suunnittelu voidaan mieltää "substantiivivetoisiksi", koska oliot ja luokat kuvaavat usein asioita, joita luonnollisessa kielessä kuvataan substantiiveilla. |
Ohjelmat saatetaan mieltää vastaavasti "verbivetoisiksi". On myös "datalähtöisempiä" vaikka oliottomia ohjelmointitapoja. |
On väitetysti ihmiselle luontevampi tapa mallintaa käsiteltävää ongelmakenttää. Käsitteellisellä mallinnuksella saattaa olla korostunut rooli. |
On väitetysti ihmiselle luontevampi tapa kuvata vaiheittaisia ratkaisualgoritmeja. |
Korostaa tiedon piilottamisen periaatetta: olioilla/luokilla on omat vastuualueet, ja niiden yksityisiin osiin (toteutukseen) ei ulkopuolelta pääse käsiksi. Tiedon piilottaminen parantaa mm. koodin muokattavuutta. |
Tiedon piilottaminen ei välttämättä toteudu yhtä hyvin, joskin tämä riippuu ratkaisevasti ohjelmointitavasta ja ohjelmoijasta. Huonosti tehtyyn proseduraaliseen ohjelmointiin liitetään perinteisesti globaalien muuttujien ja ns. spagettikoodin ongelmat. |
Yhteen liittyviä tietoja kuvataan usein olion ilmentymämuuttujina. |
Yhteen liittyviä tietoja kuvataan esimerkiksi tietueeseen kuuluvina muuttujina (tietue eli record on vähän kuin metoditon olio) tai monikkoina. |
Tarjoaa mahdollisuuden määritellä ylä- ja alakäsitteitä periytymisen avulla. |
Ei periytymistä. |
Yleisessä käytössä sekä akateemisessa että yritysmaailmassa. |
Yleisessä käytössä sekä akateemisessa että yritysmaailmassa. |
Olio-ohjelmointi tällä kurssilla
Tälle kurssille asetettuihin tavoitteisiin kuuluu olio-ohjelmoinnin opettaminen, ja se näkyy kurssin toteutuksessa. Ensimmäisen kierroksen "irtofunktioita" lukuun ottamatta lähes kaikki kurssin esimerkkiohjelmat edustavat olio-ohjelmointia.
Imperatiivinen ohjelmointi vs. funktionaalinen ohjelmointi
Deklaratiivinen ohjelmointi on siinä määrin monimääritelmäinen ja epämääräisesti rajattu kokonaisuus, että meidän on nyt mielekkäämpää verrata imperatiivista ohjelmointia sopivasti valittuun esimerkkiin deklaratiivisesta ohjelmoinnista kuin koko deklaratiiviseen paradigmaan. Vertaillaan siis nyt imperatiivista ja funktionaalista ohjelmointia.
Ratkaisevin ero: miten tilaa mallinnetaan?
Keskeisin ero imperatiivisen ja funktionaalisen ohjelmoinnin välillä on tapa, jolla tilaa mallinnetaan.
Imperatiivisessa ohjelmassa käskyn suorittaminen voi vaihtaa muuttujan arvoa, pysähtyä lukemaan syötettä tai tehdä jotakin muuta sellaista, joka näkyy käskyn suorittajalle vaikutuksena ohjelman tilaan. Käskyn suorittaminen voi myös vaikuttaa siihen, millaisia tilanmuutoksia seuraavaksi suoritettavat käskyt aiheuttavat.
Puhtaasti funktionaalisessa ohjelmassa ohjelmakoodi koostuu määrittelyistä, joilla ei ole tilaa muuttavia vaikutuksia eli "sivuvaikutuksia" tässä mielessä. Esimerkiksi muuttujan arvoa ei vaihdeta, kun se on kerran asetettu.
Seuraava taulukko kertoo lisää tästä ratkaisevasta erosta.
Imperatiivinen ohjelmointi |
(Puhtaasti) funktionaalinen ohjelmointi |
---|---|
Funktioita on mahdollista ja sopivaa laatia niin, että ne muuttavat ohjelman tallentamia tilatietoja. Esimerkiksi oliolle voidaan laatia metodi, joka muuttaa sen ilmentymämuuttujien arvoja. Funktion paluuarvoon voi vaikuttaa parametrien lisäksi myös ohjelman muuttuva tila. |
Funktiot laaditaan vaikutuksettomiksi. Ne eivät muuta olemassa olevia muuttujia. Funktio palauttaa samoilla parametriarvoilla aina saman arvon. Olemassa olevien olioiden tilaa ei muokata; kun halutaan toisentilainen olio, luodaan uusi olio, joka on halutunlainen. |
Toisin sanoen: tietorakenteet kuten oliot voivat olla muuttuvatilaisia. |
Tietorakenteet kuten oliot ovat aina muuttumattomia. |
On hyvin luontevaa mallintaa aihealueita, joihin liittyy vaihtuva tilatieto (esim. tilin saldo tai etenevä ajanhetki). |
Muuttuvan tilan mallintaminen onnistuu kyllä hyvin mutta toisella tavalla. Voidaan esimerkiksi luoda talletusten ja nostojen yhteydessä uusia tilin tilaa kuvaavia olioita, joilla on eri saldo. |
Aliohjelmia kutsutaan paitsi paluuarvojen saamiseksi myös tilan muuttamiseksi. Esimerkiksi Peräkkäiset samanlaiset kutsut voivat kukin muuttaa tilioliota. On oleellista, montako kertaa sellainen kutsu kohdistetaan samaan olioon. |
Aliohjelmia kutsutaan (vain) niiden paluuarvojen määrittämiseksi. Jos paluuarvo tiedetään, ei edes tarvitse kutsua aliohjelmaa. Minkä tahansa lausekkeen voi periaatteessa korvata sen arvolla tai toisella arvoltaan samalla lausekkeella muuttamatta ohjelman tuottamaa tulosta (ns. arvon läpinäkyvyyden periaate; referential transparency). Esimerkiksi jos kutsu Ei ole (tuloksen oikeellisuuden kannalta) oleellista, montako kertaa jokin kutsu suoritetaan. |
Esimerkiksi kun Scala-kielellä ohjelmoidaan puhtaasti funktionaalisella tyylillä,
käytetään vain val
-muuttujia (ei var
).
Funktionaalista ohjelmointia harjoitetaan usein epäpuhtaassa muodossa niin, että osa ohjelmasta on puhtaasti funktionaalinen, mutta osa on toteutettu imperatiivisesti (esim. syötteen ja tulosteen käsittely käyttöliittymässä).
Arvon läpinäkyvyyden periaate ja "vaikutukset"
Yllä mainittiin funktionaalisen ohjelmoinnin keskeiseksi periaatteeksi se, että lausekkeen voi korvata arvollaan ilman, että ohjelman tuottama tulos — ohjelman merkitys — muuttuu. Mitä tämä oikeastaan vaatii?
Ensinnäkin vaaditaan sitä, että saman lausekkeen on tuotettava joka
evaluointikerralla sama arvo. Erityisesti: funktio ei saa palauttaa eri
kutsukerroilla eri arvoa, jos sen parametrit ovat samat. Esimerkiksi
max(a, b)
toimii näin, mutta Random.nextInt(10)
ei, eikä laskuri.arvo
.
Toiseksi vaaditaan, että lausekkeen evaluoiminen ei saa aiheuttaa mitään
sellaista, joka voidaan ulkoapäin havaita ja joka on ohjelman toiminnan
kannalta merkityksellistä. Esimerkiksi n = n + 1
havaittavasti muuttaa
muuttujan arvoa ja println("Ave")
ohjelman tulostetta.
On tavalla tai toisella linjattava, mikä kaikki on ohjelman toiminnan kannalta merkityksellistä. Jotain sellaistahan teimmekin jo kurssin alussa: totesimme, että laskemme olion tilan muuttamisen ja tulosteen tuottamisen vaikutuksiksi (luvut 1.6 ja 1.7) mutta ajan kulumista ja paluuarvon muodostumista emme; tämä on tyypillinen linjanveto. Samoin emme ole laskeneet merkitykselliseksi vaikutukseksi esimerkiksi tietokoneen muistin varaamista uudelle oliolle emmekä sitä, että kone tekee koodin suorittaessaan työtä ja kuluttaa sähköä. Joissakin yhteyksissä voisi olla perusteltua lukea nämäkin tapahtumat merkityksellisiksi.
Riippuu tarkastelunäkökulmasta ja tavoitteista, mikä luetaan merkitykselliseksi vaikutukseksi ja mikä ei. Hyvä yleisperiaate on: vaikutus on merkityksellinen mikäli se, toimiiko ohjelma halutusti, riippuu siitä, tapahtuuko tuo vaikutus vai ei.
Aiheesta kerrotaan lisää esimerkiksi nettifoorumeilla ja kirjassa Functional Programming in Scala (ks. Kirjoja ja linkkejä -sivu).
"Vaikutus" terminä
Sitä, mitä tässä luvussa ja muuallakin O1:n materiaalissa kutsumme "vaikutukseksi" (effect), kutsutaan muissa yhteyksissä usein "sivuvaikutukseksi". Jos luet muita lähteitä, jotka käsittevät puhdasta funktionaalista ohjelmointia, voit törmätä "effect"-termin toiseen merkitykseen, joka ei tarkoita samaa kuin "sivuvaikutus". Ks. esim. What do “effect” and “effectful” mean in functional programming?.
Funktionaalinen I/O
Oheisesta tekstistä saattaa syntyä vaikutelma, ettei puhtaasti funktionaalinen ohjelma voi vuorovaikuttaa käyttäjänsä kanssa.
On totta, että muilta osin funktionaalisissa ohjelmissa syötteen ja tulosteen käsittely eli I/O (input/output) saatetaan hoitaa imperatiivisesti. Kuitenkin on myös kehitetty työkaluja funktionaaliseen I/O-ohjelmointiin. Vapaaehtoisena ja vaikeana lisätehtävänä voit etsiä ja opetella aiheesta lisää internetistä.
Toinen ero: korkeamman asteen funktiot
Mikä on funktioiden rooli ohjelmissa? Miten toimenpide saadaan toistettua useasti? Eri paradigmat suosivat eri vastauksia:
Imperatiivinen ohjelmointi |
Funktionaalinen ohjelmointi |
---|---|
Aliohjelmat ja niiden käsittelemä data mielletään usein kahtena erillisenä asiana. Toisaalta on metodeita/funktioita, jotka aiheuttavat toimintoja; toisaalta on muuttujia ja niiden arvoja eli dataa, johon toimintoja kohdistetaan. |
Funktiot nähdään eräänä datan muotona ja hyvin keskeisenä sellaisena. On tavallista laatia ja käyttää korkeamman asteen funktioita eli funktioita, jotka ottavat parametreiksi funktioita tai palauttavat funktioita. Paradigman nimi viittaa osin tähän. |
Käskyjä toistetaan ensisijaisesti muodostamalla silmukoita
toistokäskyillä. Toistokäskyt muokkaavat ohjelman tilaa. Tilaa
muuttavat |
Yleinen tapa toistaa käskyjä on käyttää tarkoitukseen sopivaa
korkeamman asteen funktiota kuten |
Korkeamman asteen funktioiden runsas käyttö on siis imperatiiviselle ohjelmoinnille vähemmän ominaista, vaikka tällaisia funktioita ei suinkaan esiinny ainoastaan funktionaalisessa ohjelmoinnissa. Ohjelmointiparadigmat ovat paitsi teknisiä myös kulttuurisidonnaisia asioita: osin on kyse siitä, että imperatiivista ohjelmointia harjoitettaessa on tapana tehdä eräitä asioita toisin kuin funktionaalisessa perinteessä.
Imperatiivinen ja funktionaalinen ohjelmointi tällä kurssilla
Tälle kurssille asetettuihin tavoitteisiin kuuluu imperatiivisen olio-ohjelmoinnin opettelu. Kurssimateriaalin ohjelmat ovatkin enimmäkseen imperatiivisella tyylillä kirjoitettuja. O1 kuitenkin tarjoaa esimakua myös funktionaalisesta ohjelmoinnista, johon pääset paneutumaan tarkemmin jatkokursseilla. Etenkin tapa, jolla olemme luvusta 6.3 alkaen käsitelleet kokoelmia korkeamman asteen metodeilla on funktionaaliselle ohjelmalle ominainen.
Esimerkkejä imperatiivisesta ja funktionaalisesta ohjelmoinnista
Katsotaan muutama kurssiltamme jo tuttu koodinpätkä ja mietitään, mitä tyylejä ne edustavat.
Esimerkkejä imperatiivisesta ohjelmoinnista
class Laskuri(var arvo: Int):
def etene() =
this.arvo = this.arvo + 1
override def toString = "arvo " + this.arvo
end Laskuri
Luvun 3.1 Laskuri
-luokka on esimerkki imperatiivisesta olio-ohjelmoinnista. Siinä
esiintyy var
-muuttuja, etene
-metodi aiheuttaa tilaa muuttavan "sivuvaikutuksen"
ja Laskuri
-oliot ovat muuttuvatilaisia.
Imperatiivista tapaa edustavat myös nämä (monien muiden lisäksi):
Scalan
Buffer
-luokka. Puskuriolion tilahan muuttuu, kun sen alkioita vaihdetaan, lisätään tai poistetaan.Ruudukkoja kuvaava luokka
o1.Grid
(esim. luku 8.2): ruudukon tilaa voi muuttaa vaihtamalla ruudun sisällön toiseksi.
Käyttöliittymäluokka
o1.View
, joka perustuu ajatukseen, ettäView
-olio tarjoaa näkymän yhteen aihealueen mallina toimivaan olioon, jonka tilaa muokataan tapahtumankäsittelijämetodien kautta.
Esimerkkejä funktionaalisesta ohjelmoinnista
Luvussa 2.5 hahmottelimme Pos
-luokkaa, jonka koodista on tässä osa:
class Pos(val x: Double, val y: Double):
def description = "(" + this.x + "," + this.y + ")"
def add(dx: Double, dy: Double) = Pos(this.x + dx, this.y + dy)
end Pos
Pos
-oliot ovat muuttumattomia: niillä on vain val
-muuttujia, jotka osoittavat
muuttumattomiin olioihin. Kun haluamme muodostaa uuden sijainnin suhteessa olemassa
olevaan olioon ja kutsumme add
-metodia, ei alkuperäinen olio muutu miksikään,
vaan luomme ja palautamme uuden Pos
in.
Muita esimerkkejä:
o1.Pic
-oliot ovat vastaavasti muuttumattomia. Niiden metodit (esim. luku 2.3) eivät muokkaa alkuperäistä kuvaa vaan tuottavat uuden kuvan.Odds
-luokan metodit (luku 2.5) toimivat vastaavasti.Vector
(luku 4.2) on tilaltaan muuttumaton alkiokokoelma.Peeveli-pelin
GameState
-luokka (luku 10.2) on muuttumaton sekin.
Funktionaalista tapaa edustaa myös String
-tietotyyppi. Minkään merkkijono-olion sisältö
ei koskaan muutu miksikään; kun merkkijonoja yhdistellään esimerkiksi lausekkeella
ekaTeksti + tokaTeksti
, niin syntyy pidempää tekstiä kuvaava merkkijono-olio, ja
alkuperäiset jonot jäävät ennalleen.
Esimerkki: String
vs. StringBuilder
Tähän väliin sopii mainita, että Scala tarjoaa vaihtoehtona myös luokan StringBuilder
,
joka on "imperatiivinen String
" eli kuvaa merkkijonoja, joita voi muuttaa:
val jono = StringBuilder("kissa")jono: StringBuilder = kissa jono.append("kala")jono: StringBuilder = kissakala jono.append("puikko")jono: StringBuilder = kissakalapuikko
String
on yleensä kätevämpi, mutta StringBuilder
on hyödyksi erityisesti silloin,
kun merkkijonoja yhdistellään toisiinsa valtavia määriä ja halutaan optimoida ohjelman
tehokkuutta. StringBuilder
ia käyttäessä tietokone ei joudu luomaan niin paljon olioita
eikä siivoamaan niin paljon tarpeettomiksi jääneitä olioita muistista.
Vähän paradigmojen yhdistelemisestä
On eri asia, miten luokka toimii, kuin millaisessa koodissa sitä käytetään. Esimerkiksi
String
-tyyppi edustaa funktionaalista ohjelmointitapaa, mutta String
-tyyppiä käyttäen
voi kirjoittaa imperatiivistakin koodia:
var jono = "kissa"
jono = jono + "kala" // arvo vaihtuu, joten ainakin tämä osa ohjelmasta on imperatiivinen
On myös mahdollista toteuttaa imperatiivisella tavalla sellainen luokka, joka on luokan
käyttäjän näkökulmasta funktionaalinen. Luokka voi esimerkiksi sisäisesti käyttää
paikallisia var
-muuttujia jonkin metodin toteutuksessa apuna, vaikka luokan
ilmentymät olisivatkin muuttumattomia.
Pikkuinen ohjelmointitehtävä: imperatiivisesta funktionaaliseksi
Varsinkin jos tuntuu epäselvältä, millä perusteella äsken luokittelimme kurssin esimerkkejä funktionaalisiksi, voit tehdä tämän lisätehtävän, jossa muokkaat ennestään tutun imperatiivisen koodin funktionaalisemmaksi.
Tehtävänanto
Luvun 4.4 Football3-moduulin Match
-luokkaa vastaava toiminnallisuus voidaan
toteuttaa toisinkin: sovitaan, että yksi Match
-olio ei kuvaakaan yksittäisen ottelun
vähitellen muuttuvaa tilannetta, kuten aiemmin, vaan yksittäistä, muuttumatonta otosta
ottelun tilasta. Maalin lisääminen otteluun ei muuta Match
-oliota vaan synnyttää
uuden Match
-olion, joka kuvaa uutta tilannetta.
Toteuta funktionaalinen Match
-luokka tavalla, joka on kuvattu tarkemmin moduulin
Football4 scaladoceissa.
(Muutettavaksi on tässä työmäärän rajaamiseksi valittu sellainen luokka, johon ei tarvita kuin pieniä muutoksia. Muissa tapauksissa muutos imperatiivisesta funktionaaliseksi voi olla työläämpikin.)
Ohjeita ja vinkkejä
Katso, ettei
addGoal
-metodisi palauta enääUnit
-arvoa.Kuten dokumentaatio osoittaa,
Match
-oliota luodessa ilmoitetaan nyt luontiparametrilla, ketkä ovat toistaiseksi tehneet maaleja kyseisessä ottelutilanteessa. Tähän käytetään tilaltaan muuttumatonta alkiokokoelmaa; vektori kelpaa tässä ihan hyvin. Uutta ottelutilannetta luodessa on tarpeen muodostaa uusi vektori, jossa on aiempia maalintekijöitä sekä lopussa uusin lisätty. Se onnistuu esimerkiksi vektorien operaattorilla:+
, joka toimii näin:val lukuja = Vector(5, 1, 2)lukuja: Vector[Int] = Vector(5, 1, 2) val isompi = lukuja :+ 10isompi: Vector[Int] = Vector(5, 1, 2, 10)
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Opiskelija kertoo
Tuli muuten mieleen kohtaus jostain vanhasta sotilasfarssista (Rykmentin murheenkryyni?)":
Lisää imperatiivisen ja funktionaalisen paradigman eroja
Seuraava taulukko luettelee lisää imperatiiviselle ja funktionaaliselle paradigmalle tyypillisiä piirteitä. Se liittyy jo mainittuun pääeroon eli muuttuvan tilan käsittelyyn.
Imperatiivinen ohjelmointi |
Funktionaalinen ohjelmointi |
---|---|
Ohjelmat ja niiden aliohjelmat (esim. metodit) ovat käskysarjoja. Käskyillä voi olla suoria vaikutuksia ohjelman tallentamaan tilaan. |
Aliohjelmat ovat verrattavissa matemaattisiin funktioihin. Ne vain palauttavat arvon parametriensa perusteella. (Paradigman nimi viittaa osin tähän.) |
Tyypillisiä alkiokokoelmia ovat taulukot (luku 12.1) ja tältä kurssilta jo tutut puskurit. Niiden sisältöä voi muuttaa niiden luomisen jälkeenkin. |
Arvoja säilötään muuttumattomiin alkiokokoelmiin. Näitä ovat esimerkiksi vektorit (luku 4.2), listat ja laiskalistat (luku 7.2); termistö tosin vaihtelee ohjelmointikielestä riippuen. |
Ohjelman suoritusta tarkasteltaessa on huomioitava näkymättömissä tapahtuvat tilanmuutokset ja käskyjen suoritusjärjestys. Esimerkiksi tämä käskypari: val a = olio.juttu() val b = olio.toinen() voi tuottaa eri arvot kuin tämä: val b = olio.toinen() val a = olio.juttu() Tulos voi olla erilainen, koska metodit voivat vaikuttaa ohjelman tilaan. |
Ohjelman suoritusta voi tarkastella yksinkertaisemmin. Suoritusjärjestys on ohjelmoijan näkökulmasta usein toissijaista. Esimerkiksi tämä käskypari: val a = olio.juttu val b = olio.toinen tuottaa saman tuloksen kuin tämä: val b = olio.toinen val a = olio.juttu Tulos on väistämättä sama, koska tilaa ei muokata. |
Toinen esimerkki järjestyksen merkityksestä: kutsussa
|
Kutsussa |
Viittauksen (tai muistiosoittimen) käsite on keskeinen. Viittaukset vaikuttavat siihen, mitä arvoja tietyllä lausekkeella missäkin vaiheessa ohjelman suoritusta on. Jos usea muuttuja viittaa samaan olioon, niin olion tilan muuttaminen yhden muuttujan kautta vaikuttaa toisenkin muuttujan käyttämiseen (ks. esim. luku 1.5). |
Viittauksilla ei ole samalla tavalla keskeistä merkitystä. Koska lausekkeen evaluoiminen ei vaikuta toisiin lausekkeisiin, ei voi käydä niin, että tilan muuttaminen yhden muuttujan kautta vaikuttaisi toisen muuttujan käyttämiseen. |
Tehokkuudesta
Imperatiivisista toteutuksista on historiallisesti usein saatu suoritustehokkuudeltaan funktionaalisen ohjelmoinnin toteutuksia tehokkaampia. Imperatiivinen ohjelmointitapa mahdollistaa eräänlaisia resurssioptimointeja.
Mutta myös tehokkaita funktionaalisia toteuksia on; suoritustehokkuus riippuu ohjelmasta ja käytetyistä työkaluista. Nyt moniydinprosessorien ja klusterien aikakaudella on huomionarvoista, että funktionaalisia ohjelmia voi joustavasti määrätä suoritettavaksi rinnakkaisajoina: koska funktiokutsut eivät muuta tallennettua tilaa ja koska lausekkeita voidaan evaluoida vapaammassa järjestyksessä, evaluointi voidaan jakaa eri prosessoriytimille samanaikaisesti suoritettavaksi. Rinnakkaislaskenta mahdollistaa näin merkittäviä tehokkuusetuja, joista kertoo esimerkiksi Ohjelmointi 2 -kurssin materiaali.
Luettavuus ja edut ohjelmistohankkeissa
Imperatiivinen ohjelmakoodi on — väitetysti — helppolukuisempaa kuin funktionaalinen koodi. Ohjelman suorituksen vaiheet näkyvät ohjelmakoodista suoremmin.
Funktionaalinen ohjelmakoodi on — väitetysti — helppolukuisempaa kuin imperatiivinen koodi. Funktionaalisia ohjelmia on väitetysti helpompaa kirjoittaa bugittomiksi ja helposti jatkokehitettäviksi silloinkin, kun ohjelmakokonaisuus on mutkikas. Debuggereita yms. työkaluja ei ehkä yhtä usein tarvita.
Monet funktionaalisen ohjelmoinnin edut kumpuavat siitä, että on tapana kirjoittaa paljon pieniä funktioita ja yhdistellä niitä. Tällaisten funktioiden toiminnasta on helpompi tehdä päätelmiä ja niitä on helpompi testata, kun ei tarvitse huomioida mahdollisia vaikutuksia tilaan. Tällaisista huolella laadituista palasista asiantuntevien ohjelmoijien on hyvä rakentaa vaativia, luotettavasti toimivia ohjelmistoja tosikäyttöön.
Paradigmat lintuperspektiivistä
Vielä yksi taulukko. Tässä katsotaan paradigmojen ympärille muodostettuja ajattelutapoja ja kulttuureita.
Imperatiivinen ohjelmointi |
Funktionaalinen ohjelmointi |
---|---|
Tietokoneohjelmointi nähdään koneen käskemisenä. "Sanotaan koneelle, miten sen pitää toimia." Käskemiseen käytetään ohjelmointikielen lauseita. Käskyihin kirjataan peräkkäisiä suoritusaskelia, jotka kone käy läpi ohjelmaa suorittaessaan. |
Tietokoneohjelmointi saatetaan nähdä ratkaisujen määrittelemisenä koneelle. "Kuvaillaan koneelle, mitä halutaan saada aikaan." Ohjelma-ajo on epäsuorempi seuraus näiden määrittelyjen mukaisesta toiminnasta kuin imperatiivisessa ohjelmoinnissa. (Tämä on tyypillistä deklaratiiviselle paradigmalle yleisemminkin.) |
Abstraktiotaso voi olla matala: ohjelmien rakenne saattaa vastata laitteiston toimintaa ja konekieltä suhteellisen läheisesti. (Riippuu paljon mm. ohjelmointikielestä.) |
Abstraktiotaso on korkea: ohjelmat eivät merkittävästi muistuta konekieltä. |
Ohjelmointi nähdään suhteellisen usein insinööritieteen haarana. |
Ohjelmointi nähdään suhteellisen usein matematiikan haarana. |
On väitetysti ihmisille luonnollisempi ja helpommin opittava tapa ilmaista ratkaisuja ongelmiin. |
On väitetysti ihmisille luonnollisempi ja helpommin opittava tapa ilmaista ratkaisuja ongelmiin. |
Nykyään käytetään usein yhdessä olio-ohjelmoinnin kanssa. |
Harvemmin mutta kasvavassa määrin yhdistetään myös olio-ohjelmointiin. |
Perinteinen valta-asema teollisuudessa ja harrastelijaohjelmoijien parissa. Vahva asema myös akateemisessa maailmassa. |
Suosio selvässä nousussa teollisuudessa 2010-luvulta alkaen. Vahva asema akateemisessa maailmassa. |
Tehtävä: neljä ohjelmaa, neljä paradigmaa
Alla on neljä erilaista toteutusta samalle toiminnallisuudelle: Mallinnetaan yksinkertaisia "varastoja", joissa on tietyn nimistä ainetta. Varastossa olevan aineen määrää voi kasvattaa metodikutsulla.
Vertaile toteutuksia toisiinsa ja mieti, minkä ohjelmointiparadigmojen piirteitä niihin liittyy.
Paradigmojen erot eivät pääse kunnolla oikeuksiinsa näin pienissä esimerkeissä, jotka voivat tuntua kovin samankaltaisilta. Esimerkiksi sillä, että funktionaalisessa ohjelmoinnissa vältetään tilan muokkaamista, on kauaskantoisia seuraamuksia, jotka eivät tässä pääse esiin. Joitakin paradigmoille tyypillisiä piirteitä voi näistä koodinpätkistä silti löytää.
Toteutus A
class Varasto(val aine: String, val kapasiteetti: Int, maaraAluksi: Int):
private var maara = maaraAluksi
def kapasiteettiaJaljella = this.kapasiteetti - this.maara
def lisaa(paljonko: Int) =
this.maara = min(this.maara + paljonko, this.kapasiteetti)
// ... muita metodeita ...
end Varasto
Käyttöesimerkki:
val tankki = Varasto("maitoa", 1000, 50)
tankki.lisaa(500)
val nytTilaa = tankki.kapasiteettiaJaljella
Toteutus B
class Varasto(val aine: String, val kapasiteetti: Int, val maara: Int):
def kapasiteettiaJaljella = this.kapasiteetti - this.maara
def lisaa(paljonko: Int) =
Varasto(this.aine, this.kapasiteetti, min(this.maara + paljonko, this.kapasiteetti))
// ... muita metodeita ...
end Varasto
Käyttöesimerkki:
val tankinTila = Varasto("maitoa", 1000, 50)
val uusiTila = tankinTila.lisaa(500)
val nytTilaa = uusiTila.kapasiteettiaJaljella
Toteutus C
class Varasto(val aine: String, val kapasiteetti: Int, var maara: Int)
// Ei metodeita. Varasto on yllä olevien kolmen ilmentymämuuttujan muodostama tietue.
def lisaa(kohde: Varasto, paljonko: Int) =
kohde.maara = min(kohde.maara + paljonko, kohde.kapasiteetti)
def kapasiteettiaJaljella(varasto: Varasto) = varasto.kapasiteetti - varasto.maara
// ... muita funktioita ...
Käyttöesimerkki:
val tankki = Varasto("maitoa", 1000, 50)
lisaa(tankki, 500)
val nytTilaa = kapasiteettiaJaljella(tankki)
Toteutus D
class Varasto(val aine: String, val kapasiteetti: Int, val maara: Int)
// Ei metodeita.
def lisaa(kohde: Varasto, paljonko: Int) =
Varasto(kohde.aine, kohde.kapasiteetti, min(kohde.maara + paljonko, kohde.kapasiteetti))
def kapasiteettiaJaljella(varasto: Varasto) = varasto.kapasiteetti - varasto.maara
// ... muita funktioita ...
Käyttöesimerkki:
val tankinTila = Varasto("maitoa", 1000, 50)
val uusiTila = lisaa(tankinTila, 500)
val nytTilaa = kapasiteettiaJaljella(uusiTila)
Kysymyksiä
Ohjelmointikielten sijoittuminen paradigmoihin
Usein kysyttyä: mihin paradigmaan Java / JavaScript / C / C++ / Python / Pascal kuuluu?
Java on imperatiivinen olio-ohjelmointikieli. Viimeisimmissä Java-versioissa on osittaista tukea funktionaalisemmallekin ohelmointitavalle.
Ensisijaisesti opetuskäyttöön suunniteltu Pascal eli kulta-aikaansa 1980- ja 1990-luvuilla. Se edustaa/edusti lähinnä proseduraalista paradigmaa.
C-kieli sopii proseduraaliseen ohjelmointiin. C:n pohjalta kehitetyllä C++ -kielellä ohjelmoidaan yleensä joko proseduraalisesti tai imperatiiviseen oliotyyliin.
Python on moniparadigmakieli. Sillä usein ohjelmoidaan imperatiivisesti muttei funktionaalinen tyylikään poissuljettu ole. Olioita voi käyttää; jotkut käyttävät Pythonia oliopainotteisemmin ja toiset proseduraalisemmin.
JavaScript on myös moniparadigmakieli.
Periaatteessa melkein mitä vain kieltä voi käyttää melkein minkä vain paradigman mukaisesti. Vaikka itse kielessä sinänsä ei esimerkiksi olioita olisikaan, niin voi pyrkiä ohjelmointityyliin, jolla olioita "simuloidaan"; joihinkin tällaisiin tarkoituksiin löytyy apukirjastojakin. Moinen ei vain välttämättä ole niin kätevää, että sitä tulisi käytännössä tehtyä. Jotkin kielet sopivat tiettyihin tyyleihin paremmin kuin toiset.
Eikö Scalaa sanota joskus funktionaaliseksi ohjelmointikieleksi?
Scala on moniparadigmakieli, joka on suunniteltu sellaiseksi, että sillä voi ohjelmoida usealla eri tavalla. Kuitenkin Scalassa nimenomaan olio-ohjelmointi ja funktionaalinen ohjelmointi antavat toisilleen suuta poikkeuksellisen intohimoisesti, ja kieli on siinä mukana: Scala kirjastoineen ikään kuin houkuttelee ohjelmoijaa käyttämään näitä paradigmoja ja yhdistämään niitä.
Tiettyjen toimintatapojen suosiminen näkyy Scalassa monissa
pienissä asioissa, vaikkapa siinä, että muuttumattomat
(funktionaalisemmat) Vector
-kokoelmat ovat aina käytettävissä,
mutta (imperatiivisemman) Buffer
-luokan joutuu erikseen
import
aamaan.
Melko monet käyttävät Scalaa melko puhtaasti funktionaalisena kielenä.
Kuten sanottu, Ohjelmointi 1 -kurssilla emme ole käyttäneet Scalaa erityisen funktionaalisesti. Toisaalta olemme luoneet muuttumattomiakin olioita ja käyttäneet paljon korkeamman asteen funktioita, jotka ovat funktionaalisen ohjelmoinnin leimallisia piirteitä.
Paradigmat historiaan?
Luvun alussa vihjattiin, että vaikka nykyinen paradigmajaottelu on hyvä tuntea, se on jäämässä vanhanaikaiseksi. Ja äsken oli puhetta "moniparadigmakielistä", jotka ovat siinä osallisina. Jos tämä teema kiinnostaa, voit lukea esimerkiksi Joshua Grossin lyhyen puheenvuoron Quora-sivustolla, ja verkosta löytyy toki paljon muutakin.
Luonnollisuudesta
Mikä on "luonnollinen" tapa ohjelmoida?
Luvussa 2.1 sanottiin käsitteiden mallintamisen (esim. olioina) olevan eräässä rajallisessa mielessä "ihmislähtöistä".
Ylempänä tässä todettiin, että eri paradigmojen "luonnollisuudesta" on esitetty keskenään ristiriitaisia väitteitä.
Oli hauskaa, että paradigmojen vertailussa kävi ilmi se, että eri ohjelmointitapojen kannattajat perustelevat näkemyksiään samanlaisilla subjektiivisilla näkemyksillä: "Imperatiivinen/funktionaalinen ohjelmakoodi on — väitetysti — helppolukuisempaa kuin funktionaalinen/imperatiivinen koodi."
Otetaan noiden väitteiden arvioimisen sijaan askel taaksepäin ja katsotaan kaikkia käsiteltyjä paradigmoja yhtenä kokonaisuutena.
Tarvitseeko ohjelmoinnin yleensäkään olla nykyisen kaltaista? Millainen tapa dynaamisten prosessien kuvaamiseen hyödyntäisi ihmislajin koko potentiaalin? Voisiko jonkinlainen ohjelmointi olla niin luonnollista, että ihmiset voisivat luoda pieniä ohjelmia sekunneissa keskustelun tai ajattelun lomassa ja osaksi? Millaiseen ajatteluun ihminen voisi yltää, jos käytettävissä olisi sellainen media?
Seuraava video saattaa innoittaa niitä, jotka ovat kiinnostuneet ohjelmoinnista, tulevaisuudesta, käyttöliittymistä, medioista ja/tai ajattelusta. Suosittelen myös Bret Victorin muuta tuotantoa.
Yhteenvetoa
Monien puheissa ja ajatuksissa ohjelmoinnin kenttä jakautuu paradigmoihin eli toisistaan poikkeaviin ohjelmointitapoihin.
Ohjelmointiparadigmat ovat osin sisäkkäisiä ja osin muuten päällekkäisiä; niillä ei ole yksiselitteisiä määritelmiä. Paradigmoja voi yhdistellä yhden ohjelman sisälläkin.
Eräs paradigmajako voidaan nähdä imperatiivisen ja deklaratiivisen ohjelmoinnin välissä:
Imperatiivisessa ohjelmoinnissa komennetaan tietokonetta peräkkäisten käskyjen sarjoilla. Sen valtavirtaan kuuluvat proseduraalinen ohjelmointi ja olio-ohjelmoinnin yleisimmät muodot.
Deklaratiivisessa ohjelmoinnissa ohjelmoija keskittyy pikemminkin ratkaisun osien välisiin suhteisiin kuin käskyjen järjestykseen.
Tällä kurssilla harjoitamme ensisijaisesti imperatiivista olio-ohjelmointia, mutta saamme tuntumaa myös funktionaaliseen paradigmaan.
Funktionaalinen ohjelmointi on eräs deklaratiivinen ohjelmoinnin keskeinen muoto.
Olio-ohjelmiakin voi kirjoittaa funktionaalisesti. Scala-kieli kannustaa tähän.
Funktionaalista ja imperatiivista ohjelmointia erottaa perustavimmin se, että (puhtaassa) funktionaalisessa ohjelmoinnissa kirjoitetaan funktioita, jotka vain palauttavat arvon eivätkä muuta ohjelman tallentamia tilatietoja tai riipu muuttuvasta tilasta.
Toisena pääerona voidaan pitää sitä, että funktionaalisessa ohjelmoinnissa on erityisen yleistä käyttää funktioita toisten funktioiden parametreina ja paluuarvoinakin.
Esimerkiksi jo usein käyttämämme korkeamman asteen metodit (
map
,filter
jne.) ovat funktionaaliselle ohjelmoinnille ominaisia.
Eri paradigmoilla on omat hyvät ja huonot puolensa. Jyrkät jaot paradigmojen välillä saattavat tulevaisuudessa loiventua. Pidä avoin mieli, kun tulevina vuosina kartutat ohjelmointikokemustasi. Älä sorru liian dogmaattiseen ajatteluun!
Lukuun liittyviä termejä sanastosivulla: ohjelmointiparadigma, olio-ohjelmointi, imperatiivinen ohjelmointi, funktionaalinen ohjelmointi, proseduraalinen ohjelmointi; muuttuvatilainen, muuttumaton, vaikutuksellinen funktio, vaikutukseton funktio.
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.
Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.