Tämä kurssi on jo päättynyt.

Kurssin viimeisimmän version löydät täältä: O1: 2024

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).

../_images/person11.png

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ä:

  1. Miten olio-ohjelmointi eroaa muusta ohjelmoinnista?
  2. Miten imperatiivinen ohjelmointi eroaa funktionaalisesta ohjelmoinnista?

Olio-ohjelmointi vs. olioton ohjelmointi

../_images/paradigms_oop_nonoop-fi.png

Seuraavaksi keskenään vertailtavat osat on korostettu tässä paradigmakartassa keltaisilla reunuksilla.

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.

../_images/paradigms_imp_func-fi.png

Seuraavaksi vertailemme keskenään tässä keltaisella kehystettyjä osia.

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.
  • 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 Posin.

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. StringBuilderia 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?)":

Imperatiivinen alikersantti: Alokas, korjatkaa asentonne.
Funktionaalinen alokas: Mitä tuota vanhaa korjoomaan. Minä teen kokonaan uuden.

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ä

Mitkä kaksi esitellyistä toteutuksista edustavat parhaiten olio-ohjelmointia?
Esitellyistä toteutuksista kahdessa on selviä imperatiivisen ohjelmoinnin piirteitä. Missä kahdessa?
Toteutuksista kaksi sopivat yhteen funktionaalisen ohjelmointiparadigman kanssa. Mitkä kaksi?
Mikä neljästä toteutuksesta vastaa lähimmin proseduraalista ohjelmointia?

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 importaamaan.

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.

a drop of ink
Palautusta lähetetään...