Kurssin viimeisimmän version löydät täältä: O1: 2024
- CS-A1110
- Kierros 10
- Luku 10.2: Ohjelmointiparadigmoista
Luku 10.2: Ohjelmointiparadigmoista
Tästä sivusta:
Pääkysymyksiä: Mitä, jos ei käytettäisi olioita? Voiko tosiaan ohjelmoida muokkaamatta ohjelman tilaa suoraan? Millaisia ohjelmointisuuntauksia on olemassa? Mistä ohjelmoijat puhuvat, kun he puhuvat paradigmoista?
Mitä käsitellään? Ohjelmointiparadigmoja. Olio-ohjelmointi, imperatiivinen ohjelmointi, funktionaalinen ohjelmointi.
Mitä tehdään? Luetaan. Pieniä tehtäviä.
Suuntaa antava työläysarvio:? Tunti.
Pistearvo: B5.
Oheisprojektit: Vapaaehtoiseen tehtävään liittyy Football4 (uusi).
Johdanto
Kauan sitten kakkoskierroksella mainittiin, 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 usein isoja tunteita. Tämän luvun aiheista onkin 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. Harmillisen usein tosin käytetty todistusaineisto perustuu vain henkilökohtaisiin vaikutelmiin.
Voi 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, eikä sitä voi tässä käsitellä läheskään tyhjentävästi. Asiaa ei helpota, 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ä yllä esitetty paradigmakaavio 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. Erityisesti (huonoon) proseduraaliseen ohjelmointiin liitetään perinteisesti globaalien muuttujien ja "spagettikoodin" ongelma |
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. Lukuunottamatta ensimmäisen kierroksen "irtofunktioita" 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 tässä kohden 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?
Imperatiivinen ohjelmointi | (Puhtaasti) funktionaalinen ohjelmointi |
---|---|
Ohjelmakoodin käskyn suorittaminen voi vaihtaa muuttujan arvoa tai pysähtyä lukemaan syötettä (tms.) sellaisella tavalla, joka näkyy käskyn suorittajalle vaikutuksena ohjelman tilaan. Käskyn suorittaminen voi vaikuttaa siihen, millaisia tilanmuutoksia seuraavaksi suoritettavat käskyt aiheuttavat. | Ohjelmakoodi koostuu määrittelyistä, joilla ei ole tilaa muuttavia vaikutuksia eli "sivuvaikutuksia"→sivuvaikutus tässä mielessä. Esimerkiksi muuttujan arvoa ei vaihdeta, kun se on kerran asetettu. |
Funktioita on siis mahdollista laatia niin, että ne muuttavat ohjelman tallentamia tilatietoja. Esimerkiksi oliolle voidaan laatia metodi, joka muuttaa sen ilmentymämuuttujien arvoja. Funktion palautusarvoon 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. |
Aihealueiden, joihin liittyy vaihtuva tilatieto (esim. tilin saldo tai etenevä ajanhetki) mallintaminen onnistuu hyvin luontevasti. | Muuttuvan tilan mallintaminen onnistuu kyllä mutta toisella tavalla. Voidaan esimerkiksi luoda talletusten ja nostojen yhteydessä uusia tilin tilaa kuvaavia olioita, joilla on eri saldo. |
Aliohjelmia kutsutaan paitsi palautusarvojen saamiseksi myös
tilan muuttamiseksi. Esimerkiksi pankkitili.nosta(1000) muuttaa
tiliolion tietoja. Vaikka tämä kutsu sattuisikin palauttamaan
luvun 500, niin on ohjelman toiminnan kannalta merkityksellistä,
kirjoitatko koodiin val saatiin = 500 vai val saatiin =
pankkitili.nosta(1000) . Peräkkäiset samanlaiset kutsut voivat
kukin muuttaa tilioliota, ja on oleellista, montako kertaa
sellainen kutsu kohdistetaan samaan olioon. |
Aliohjelmia kutsutaan (vain) niiden palautusarvojen
määrittämiseksi. Jos palautusarvo tiedetään, ei edes tarvitse
kutsua aliohjelmaa. Mikä tahansa lauseke voidaan periaatteessa
korvata sen arvolla tai toisella arvoltaan samalla
lausekkeella muuttamatta ohjelman tuottamaa tulosta (ns.
referentin_läpinäkyvyyden periaate;
referential transparency). Esimerkiksi jos kutsu tulos(10)
palauttaa luvun 123, niin koodiin voitaisiin yhtä hyvin
kirjoittaa literaali 123 kuin tuo funktiokutsu. 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 siten, että osa ohjelmasta on puhtaasti funktionaalinen, mutta osa on toteutettu imperatiivisesti (esim. syötteen ja tulosteen käsittely käyttöliittymässä).
Referentin läpinäkyvyys 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 palautusarvon 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).
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. Haastavana vapaaehtoisena lisätehtävänä voit etsiä aiheesta lisätietoa 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 for -silmukat (luku 5.5) edustavat imperatiivista
ohjelmointitapaa, samoin do - ja while -silmukat (luku 8.3). |
Yleinen tapa toistaa käskyjä on käyttää tarkoitukseen sopivaa
korkeamman asteen funktiota kuten map , filter , takeWhile
tai foldLeft , jotka eivät muuta alkuperäisen kokoelman tilaa.
Muita vaihtoehtoja ovat generaattorilausekkeet (ei käsitellä
kurssilla) ja rekursio (luku 12.1). do - ja while -silmukoita
(luku 11.1) ei käytetä; niillä ei voi tehdä oikein mitään
järkevää, jos tilan muokkaamista "sivuvaikutuksilla" ei sallita. |
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. Kurssi kuitenkin tarjoaa esimakua myös funktionaalisesta ohjelmoinnista, johon pääset paneutumaan tarkemmin jatkokursseilla. Erityisesti 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
}
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 (monien muiden lisäksi) myös:
- Scalan
Buffer
-luokka. Puskuriolion tilahan muuttuu, kun sen alkioita vaihdetaan, lisätään tai poistetaan. - Ruudukkoja kuvaava luokka
o1.Grid
(esim. luku 8.1): ruudukon tilaa voi muuttaa vaihtamalla ruudun sisällön toiseksi.
Lisätieto
On olemassa myös luokka o1.gui.immutable.View
,
joka tarjoaa toisenlaisen, funktionaalisesti
toteutettujen mallien kuvaamiseen sopivamman
käyttöliittymätoteutuksen.
- 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) = new Pos(this.x + dx, this.y + dy)
}
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 9.3) 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 = new 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ä
Huomaa, että 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 on mahdollista kirjoittaa myös imperatiivista 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-projektin 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 projektin
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 konstruktoriparametrilla, 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 helposti 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)
Palauttaminen
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ä, jotka liittyvät jo mainittuun pääeroavaisuuteen 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 11.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), virrat (luku 7.1) ja listat (luku 11.2); termistö tosin vaihtelee ohjelmointikielestä riippuen. |
Ohjelman suoritusta tarkasteltaessa on huomioitava mahdolliset
tilanmuutokset ja käskyjen suoritusjärjestys. Esimerkiksi
lausekkeen olio.juttu() + olio.toinen() arvo voi olla eri
kuin lausekkeen olio.toinen() + olio.juttu() , jos mainituilla
metodeilla on vaikutuksia ohjelman tilaan. Myös kutsussa
olio2.metodi(olio.juttu(), olio.toinen()) voi olla oleellista,
että vasemmanpuoleinen parametrilauseke evaluoidaan ensin. |
Ohjelman suoritusta voi tarkastella yksinkertaisemmin.
Suoritusjärjestys on ohjelmoijan näkökulmasta usein toissijaista.
Esimerkiksi lausekkeen olio.juttu + olio.toinen tuottaa saman
tuloksen kuin olio.toinen + olio.juttu , koska vaikutuksia
tilaan ei ole. Metodikutsussa olio2.metodi(olio.juttu, olio.toinen)
ei ole samanlaista merkitystä sillä, evaluoidaanko ensin
ensimmäinen vai toinen parametrilauseke. |
Viittauksen (tai muistiosoittimen) käsite on keskeinen, ja 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ä. Tilannetta, jossa yhden muuttujan arvon käyttäminen vaikuttaa toisen muuttujan arvon käyttämiseen, ei synny, koska minkään lausekkeen evaluointi ei vaikuta minkään toisen lausekkeen arvoon. |
Toteutuksista on perinteisesti usein saatu suoritustehokkuudeltaan funktionaalisen ohjelmoinnin toteutuksia tehokkaampia. Imperatiivinen ohjelmointitapa mahdollistaa eräänlaisia resurssioptimointeja. | Myös tehokkaita 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ä, niin evaluointi voidaan tehdä vapaammin samanaikaisesti eri prosessoriytimissä. Rinnakkaislaskenta mahdollistaa näin merkittäviä tehokkuusetuja, joista kertoo esimerkiksi Ohjelmointi 2 -kurssin materiaali. |
Ohjelmakoodi on väitetysti helppolukuisempaa kuin funktionaalinen koodi. Ohjelman suorituksen vaiheet näkyvät ohjelmakoodista suoremmin. | Ohjelmakoodi on väitetysti helppolukuisempaa kuin imperatiivinen koodi. Ohjelmia on väitetysti helpompaa kirjoittaa bugittomiksi ja helposti jatkokehitettäviksi. Tämä mm. siksi, että on tapana kirjoittaa paljon pieniä yhdisteltäviä funktioita, joiden toiminnasta on helpompi tehdä päätelmiä ja joita on helpompi testata, kun ei tarvitse huomioida mahdollisia vaikutuksia tilaan. Debuggereita yms. työkaluja ei väitetysti yhtä usein tarvita. |
Katsotaan vielä näitä paradigmoja ympäröiviä kulttuureita lintuperspektiivistä:
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 käydään läpi ohjelmaa ajettaessa. | 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. Vahva asema myös akateemisessa maailmassa. | Vahva asema akateemisessa maailmassa. Suosio selvässä nousussa myös teollisuudessa. |
Tehtävä: neljä ohjelmaa, neljä paradigmaa
Alla on neljä erilaista toteutusta samalle toiminnallisuudelle: mallinnetaan yksinkertaisia "varastoja", joissa on tietyn nimistä ainetta jokin kasvatettavissa oleva määrä. 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 tilan muokkaamisen välttämisellä funktionaalisessa ohjelmoinnissa on kauaskantoisia seuraamuksia, jotka eivät tässä pääse esiin. Eräitä paradigmoille tyypillisiä piirteitä voi näistä koodinpätkistä kuitenkin havaita.
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 ...
}
Käyttöesimerkki:
val tankki = new 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) =
new Varasto(this.aine, this.kapasiteetti, min(this.maara + paljonko, this.kapasiteetti))
// ... muita metodeita ...
}
Käyttöesimerkki:
val tankinTila = new 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 = new 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) =
new 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 = new 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 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ä.
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 palautusarvoinakin.
- 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!
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: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä ja Aleksi Vartiainen.
Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.
Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista 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.
Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.