(toiminnot)

hwechtla-tl: Ohjelmointiparadigmat

Kierre.png

Mikä on WikiWiki?
nettipäiväkirja
koko wiki (etsi)
viime muutokset


On olemassa monta erilaista tapaa ohjelmoida. Ohjelmoijana voi kehittyä tutustumalla uusiin ohjelmointitapoihin. Ohjelmointiparadigmat ovat keskenään täysin erilaisia tapoja kirjoittaa ohjelma.

Ohjelmoijien työssä luodaan asioita tyhjästä. Ohjelmoijan tuottaman koodin alkuperä on aina ohjelmoijan ajatuksissa, vaikka ohjelmoitaessa toki myös muokataan vanhaa koodia ja joskus uusi ohjelma kirjoitetaan aihion tai esimerkkikoodin pohjalta. Ei ole harvinaista, että ohjelmoija tuottaa sivukaupalla ohjelmakoodia ohjeenaan vain oma intuitio ja kokemus -- sekä jonkinlainen määrittely siitä, mitä tuloksena olevan ohjelman olisi tarkoitus saada aikaan.

Koska ohjelmat syntyvät puhtaasti ohjelmoijien ajatuksista, ohjelmoijien ajattelutavoilla on valtava vaikutus siihen, millaisia ohjelmat ovat. Samalla tavoin toimivan ohjelman voi kirjoittaa äärettömän monella eri tavalla. Nämä erilaiset lähestymistavat tuottavat ohjelmia, jotka ovat eri pituisia ja ottavat huomioon juuri tiettyjä ohjelman laajennus- ja muutostapoja. Hyvinkin monimutkaiselta vaikuttava ongelma tai algoritmi saattaa toisella tavoin hahmotettuna olla niin yksinkertainen, että sen saa ratkaistuksi lyhyellä, muutaman rivin ohjelmalla.

Ohjelmointiparadigma (programming paradigm) merkitsee ajatusmallia, jota pystyy soveltamaan mihin hyvänsä ohjelmointitehtävään.[[ALAVIITE: Sen sijaan ajatusmalleja kutsutaan yleensä suunnittelumalleiksi (design patterns), jos ne soveltuvat vain tiettyyn tilanteeseen tai ongelmatyyppiin.]] Paradigmat on todistettu yhtäpitäviksi siten, että yhden paradigman ohjelmat on muunnettavissa toisen paradigman ohjelmiksi automaattisella muunnoksella. Eri paradigmat toki tuottavat erilaisia ja eri tavoin siistejä ratkaisuja kuhunkin ongelmaan.

Ohjelmointiparadigmat yhdistetään usein ohjelmointikieliin siten, että sanotaan kustakin kielestä, mitä paradigmaa se "edustaa". Useimmat nykyaikaiset kielet ovat kuitenkin multiparadigmaattisia eli niillä on mahdollista kirjoittaa kohtuullisella vaivalla useamman paradigman mukaisesti järjesteltyjä ohjelmia. Tyypillinen valtavirtakieli (esimerkiksi C, Java tai Python) tukee siedettävällä vaivannäöllä ainakin funktionaalista, oliopohjaista ja proseduraalista lähestymistapaa. Lisäksi paradigmat eivät aina sulje toisiaan pois, vaan ohjelman eri osat voivat olla eri tyylillä kirjoitettuja.

Erilaisten ohjelmointitapojen perusteellinen läpikäynti on valtava työ, minkä vuoksi tutustumme tässä vain muutamiin keskeisimpiin ohjelmointiparadigmoihin. Tässä jutussa käsitellään seuraavia paradigmoja:

Harmillista kyllä, useimmat näistä paradigmoista eivät ole kovin hyvin määriteltyjä, eikä termien tarkoista merkityksistä vallitse yksimielisyyttä. Tätä moninaisuutta ei pysty kuitenkaan esittelemään jutussa, joten jokaiselle termille on valittu yksi, melko laveasti hyväksytty merkitys.

Imperatiivinen paradigma

Imperatiivisessa paradigmassa ohjelman suoritus etenee selkeästi tietyssä järjestyksessä, ja kullakin hetkellä ohjelmalla on tietty tila, jota ohjelman erilaiset komennot käsittelevät. Tämä paradigma on useille ohjelmoijille kaikkein tutuin, koska imperatiivista ohjelmointia tukevia (ja muut ohjelmointitavat hankalaksi tekeviä) kieliä on ollut olemassa hyvin varhaisesta vaiheesta. Imperatiivinen ohjelmointi soveltuu erityisen hyvin ainakin lähellä tietokoneen laitteistoa olevaan ohjelmointiin - esimerkiksi laiteajureihin - sekä ohjelmiin, joiden täytyy tehdä asiat tietyssä järjestyksessä tai antaa takeita vasteajoista, muistinkulutuksesta tai muusta vastaavasta.

Imperatiivinen paradigma muistuttaa hyvin paljon sitä, miten todelliset, ohjelmia ajavat tietokoneet oikeasti toimivat. Monet varhaiset ohjelmointikielet ovat tarjonneet alla olevan koneen melko suoraviivaisesti näkyville. Näissä kielissä toteuttavan koneen imperatiivinen ohjelmointi on usein helpoin ja suoraviivaisin tapa kirjoittaa ohjelmia. Suuri osa moderneista ohjelmointikäytännöistä on kehittynyt niistä abstraktiokerroksista ja keinoista, joilla ohjelmoijat ovat pyrkineet järjestelemään ja selkeyttämään koodiaan sekä helpottamaan sen ylläpitoa. Nämä kaikki abstraktiokerrokset - strukturaalinen ohjelmointi, proseduraalinen ohjelmointi, oliopohjainen ohjelmointi tunnetuimpina - ovat ohjeita, jotka toisaalta rajoittavat sitä, millaisia ohjelmia saa kirjoittaa, mutta toisaalta vastineeksi tekevät ohjelmista lyhyempiä, helpompia ymmärtää ja helpompia todistaa toimiviksi.

Strukturaalinen ja proseduraalinen ohjelmointi

Strukturaalinen ja proseduraalinen ohjelmointi ovat molemmat rajoituksia sille, milloin ohjelman suoritus saa "hypätä" uuteen kohtaan ohjelmakoodissa. Nykyisille ohjelmoijille strukturaalinen ja proseduraalinen lähestymistapa ovat lähes itsestään selviä, eikä niiden vastakohtaa, perinteistä GOTO- tai jmp-ohjelmointia, kannattane mainita muuten kuin historiallisena kuriositeettina.

Strukturaalisessa ohjelmoinnissa edellytetään, että hyppyjä tehdään vain osana ehtorakenteita ja toistorakenteita, jotka eivät saa lomittua keskenään (listaus X).


Listaus X. suurimman yhteisen tekijän etsintä.
Ohjelman tila on molemmissa muuttujien a, b ja c sisältö kunakin suoritushetkenä, sekä tieto siitä, millä rivillä ohjelmaa edetään.
Toteutuskieli: BASIC, FORTRAN.

Esistrukturaalinen (BASIC):             Strukturaalinen (FORTRAN):

10 INPUT a, b                           READ(*,*)  a, b
20 c = a                                IF (a < b) THEN
30 IF a < b THEN 60                        c = a
40 c = a MOD b                             a = b
50 IF c = 0 THEN 90                        b = c
60 a = b                                END IF
70 b = c                                DO
80 GOTO 40                                 c = MOD(a, b)
90 PRINT "syt: " b                         IF (c == 0) EXIT
                                           a = b
                                           b = c
                                        END DO
                                        WRITE(*,*) 'syt: ', b

Proseduraalinen ohjelmointi puolestaan tarkoittaa, että ohjelma jaotellaan useiksi aliohjelmiksi, jotka tekevät hyvin määriteltyjä tehtäviä ja jotka kutsuvat toisiaan tarvittaessa halutun tehtävän suorittamiseksi (listaus X). Tällä tavoin koodia saatiin usein siistityksi, kun toistuva koodi saatiin siirretyksi omaan aliohjelmaansa. Mikä ehkä vieläkin tärkeämpää, ohjelmakoodissa oli nyt yleensä nimi erilaisille toiminnoille. Alkuaikoina moni kieli ei kuitenkaan tukenut sitä, että aliohjelma saisi kutsua itseään, minkä vuoksi rekursio, tyypillinen funktionaaliseen paradigmaan kuuluva ohjelmointitekniikka, ei ollut mahdollista.


Listaus X. shellsort-järjestelyalgoritmi.
Toteutuskieli: C.

Ilman aliohjelmia:                      Aliohjelmien kanssa:
                                        void shell_sort (int *a, int length)
int a[] = {1, 3, 2, -7, 9, 4, -1, 5};   {
int length = sizeof a / sizeof a[0];      int step;
int step, i, j, tmp;                      for (step = length; step /= 2;)
for (step = length; step /= 2;) {           insertsort_by_step(step, a, length);
  for (i = step; i < length; i++) {     }
    tmp = a[i];
    for (j = i;                         void insertsort_by_step(int step,
       j >= step && tmp < a[j - step];                          int *a, int length)
       j -= step)                       {
      a[j] = a[j - step];                 int i;
    a[j] = tmp;                           for (i = step; i < length; i++)
  }                                         insert_element_at(i, a, step);
}                                       }

                                        void insert_element_at(int i, int *a, int s)
                                        {
                                          int j, tmp;
                                          tmp = a[i];
                                          for (j = i; j >= s && tmp < a[j-s]; j -= s)
                                            a[j] = a[j - step];
                                          a[j] = tmp;
                                        }

Olio-ohjelmointi

Strukturaalisen ja proseduraalisen ohjelmointitavan avulla pärjättiin muutamia vuosikymmeniä. Ne täyttävät hyvin tehtävänsä ohjelman suoritusjärjestyksen selkeyttäjinä. Ajan mittaan kuitenkin huomattiin, että ohjelman suorituksen eteneminen ei ollut ainoa vaikeaselkoinen asia ohjelmoinnissa. Oli tiettyjä asioita, jotka säännöllisesti aiheuttivat ohjelmointivirheitä suurissa ohjelmistoissa ja vaikeuttivat niiden ylläpitoa.

Imperatiiviset ohjelmat laskevat asioita päivittämällä johdonmukaisesti ohjelman tilaa, käytännössä muuttujien arvoja ja muistissa olevien tietorakenteiden sisältöjä. Huomattiin, että useat ohjelmavirheet johtuivat siitä, että ohjelman eri osat muuttelivat samaa muuttujaa tai tietorakennetta toisistaan tietämättä. Ajatellaan esimerkiksi aliohjelmaa, joka käy läpi lukulistaa (laskeakseen siitä jotain) ja tekee jonkin aliohjelmakutsun jokaisen listan elementin kohdalla. Jos kyseinen aliohjelma sivuvaikutuksenaan muokkaa läpikäytävää listaa, läpikäynnin lopputulos saattaa olla aivan muuta kuin ohjelmoija ajatteli.

Olio-ohjelmoinnissa tietorakenteita käsittelevät aliohjelmat yhdistetään tietorakenteisiinsa siten, että vain tietyt aliohjelmat, metodit, saavat muutella tiettyä tietorakennetta. Tietorakenteesta tulee tällä tavoin itsenäinen yksikkö, olio. Kukin metodi pystyy osaltaan varmistamaan, että tietorakenne on eheässä ja toimivassa tilassa, kun ohjelman suoritus luovutetaan tietorakenteen metodien ulkopuolelle. Samalla ohjelmien ylläpidettävyys paranee: tietorakenteen sisäistä toteutusta muutettaessa ei tarvitse korjata koko ohjelmaa käyttämään uusia rakenteita, vaan riittää päivittää tietorakenteen omat metodit.

Proseduraalinen ohjelmoinnin avulla tehtiin hyödyllisiä aliohjelmia ja käytettiin niitä sitten uudestaan ja uudestaan osana erilaisia suurempia ohjelmointiprojekteja. Olio-ohjelmointi avasi oven tietorakenteiden vastaavanlaiselle uudelleenkäytölle. Lisäksi olio-ohjelmoinnin myötä tuli huomattavasti helpommaksi kirjoittaa korkean tason koodia: oliota käsittelevä aliohjelma pystyy käyttämään sokeasti olion metodeja, jolloin se toimii minkä tahansa olion kanssa, joka toteuttaa oikein samat metodit. Näin ohjelmien toiminnallisuutta saattoi jälkikäteen laajentaa uusilla tietotyypeillä (listaus X).


Listaus X. listan summa.
Olioton versio käyttää indeksejä, oliollisessa on erillinen listan läpikäyjäolio. Jälkemmäinen summa tarvitsee toimiakseen ListaIteraattori-olion, mutta toisaalta pystyy käyttämään mitä tahansa oliota joka toteuttaa metodit available() ja next().
Toteutuskieli: Python.

Ilman oliota:                   Olion kanssa:

def summa(lista):               def summa(kokoelma):
  i, tulos = 0, 0                 tulos = 0
  while i < len(lista):           while kokoelma.available():
    tulos = tulos + lista[i]        tulos = tulos + kokoelma.next()
    i = i + 1                     return tulos
  return tulos
                                class ListaIteraattori:
summa([1, 2, 4, 9])               def __init__(s, lista):
                                    s.index = 0
                                    s.lista = lista
                                  def available(s):
                                    return s.index < len(s.lista)
                                  def next(s):
                                    cur = s.lista[s.index]
                                    s.index = s.index + 1
                                    return cur

                                summa(ListaIteraattori([1,2,4,9]))

Joskus luullaan, että oliopohjaista ohjelmointia varten ohjelmointikielessä pitää olla erityinen tuki erilaisille oliorakenteille. Todellisuudessa oliopohjaista koodia pystyy kirjoittamaan lähes kaikilla yleisessä käytössä olevilla ohjelmointikielillä (listaus X) matalan tason konekielistä lähtien.


Listaus X. shape-rajapinta ja sen circle-toteutus.
Toteutuskieli: Python, Scheme.
Python-kielessä on valmis tuki oliorakenteille, Schemessä ei.

Python-toteutus:                        Scheme-toteutus:

class Shape:                            (define (make-shape area border)
  def __init__(s): raise NotImplemented   (vector area border))
  def area(s): raise NotImplemented     (define (shape-area shape)
  def border(s): raise NotImplemented     ((vector-ref shape 0)))
                                        (define (shape-border shape)
class Circle(Shape):                      ((vector-ref shape 1)))
  def __init__(s, radius):
    s.radius = radius                   (define (make-circle radius)
  def area(s):                            (define (circle-area) (* pi radius radius))
    return math.pi * s.radius * s.radius  (define (circle-border) (* 2 pi radius))
  def border(s):                          (make-shape circle-area circle-border))
    return 2 * math.pi * s.radius
                                        (define myshape (make-circle 3))
myshape = Circle(3)                     (display "Border length is ")
print "Border length is",               (display (shape-border myshape))
print myshape.border()                  (newline)

Olio-ohjelmointi soveltuu erityisen hyvin tilanteisiin, joissa halutaan tuottaa valmiskäyttöisiä tietorakenteita tai muita palveluita tulevien ohjelmien käyttöön. Oliopohjaista ohjelmaa pystyy helposti laajentamaan uusilla tietotyyppitoteutuksilla - esimerkiksi peliin pystyy helposti lisäämään uusia tekoälyjä tai esineitä.

Funktionaalinen paradigma

Funktionalisessa paradigmassa käsitellään toisistaan riippumattomia arvoja (esimerkiksi lukuja, merkkijonoja tai listoja) sen sijaan, että muuteltaisiin yhden suuren koneen tilaa (todellisen tai virtuaalisen). Ohjelmat määritetään funktioina. Ne ovat tarkkoja määrityksiä siitä, kuinka arvoista lasketaan uusia arvoja: yksi funktio esimerkiksi laskee listasta uuden listan, jossa on samat elementit mutta päinvastaisessa järjestyksessä. Monimutkaiset funktiot määritellään yhdistelemällä yksinkertaisempia funktioita komposition avulla.

Intuitiivisesti voi ajatella, että funktiot ovat kuin koneita, joihin menee sisään tiettyjä arvoja (syötteitä) ja jotka tuottavat niistä toisia arvoja (tuloksia). Allegoria tosin ontuu sikäli, että funktiot eivät "kuluta" syötettään vaan saman syötteen voi rauhassa antaa vaikka kuinka monelle funktiolle. Juuri tämä ominaisuus erottaa funktionaalisen ohjelmoinnin imperatiivisesta: imperatiivisessa ohjelmoinnissa laskennan osana usein muutellaan jo olemassa olevia muuttujia tai tietorakenteita, mutta funktionaalisessa ohjelmoinnissa vain tuotetaan uusia arvoja ja tietorakenteita vanhojen perusteella. Vanhat heitetään automaattisesti pois, kun niitä ei enää tarvita.

Funktionaalinen ohjelmointi sopii erityisen hyvin tiedon muunnoksiin ja prosessointiin. Lisäksi funktionaalinen koodi on paikallaan siellä, missä koodin virheettömyys on tärkeää. Funktionaalisista ohjelmista on helpompaa todistaa erilaisia ominaisuuksia ja ne ovat yleensä jonkin verran lyhyempiä kuin imperatiiviset vastaavat ohjelmat.

Rekursio

Imperatiivisesta ohjelmoinnista tuttuja toistorakenteita ei funktionaalisessa ohjelmoinnissa voi sellaisenaan olla. Toiston päättyminen nimittäin riippuu eri muuttujien tiloista, eikä funktionaalisessa ohjelmassa muuttujan arvo enää perustamisen jälkeen muutu. Mielivaltaisen kokoisia tietorakenteita, kuten listoja, käsitelläänkin funktionaalisissa ohjelmointikielissä rekursion tai rekursioon perustuvien funktioiden avulla. Rekursio tarkoittaa, että funktio käyttää määrittelyssään avuksi itseään (listaus X). Rekursio on hyvin yleispätevä ohjelmointitekniikka, joka käy erityisen hyvin rekursiivisten tietorakenteiden (kuten puiden ja tiedostojärjestelmien) läpikäyntiin tai eri mahdollisuuksien tutkimiseen esimerkiksi tekoälyssä.


Listaus X. Listan summa.
Toteutuskieli: Python.

Iteratiivinen:          Rekursiivinen:

def summa(lista):       def summa(lista):
  tulos = 0               if len(lista) == 0:
  for e in lista:           return 0
    tulos += e            return lista[0] + \
  return tulos              summa(lista[1:])

Tietorakenteiden muuttumattomuus ja jakaminen

Koska funktiot eivät koskaan muokkaa syötteeksi saamiaan tietorakenteita vaan luovat aina uusia, saattaa tuntua, että funktionaalinen ohjelmointitapa kuluttaa hyvin paljon muistia. Tosiasiassa kuitenkin yhtä tietorakennetta voi käyttää toisen osana ja monet tietorakenteet voivat jakaa keskenään osia, jos tietorakenteiden muuttumattomuuteen voi luottaa. Usein on mahdollista säilyttää sekä alkuperäinen että muodostaa uusi tietorakenne hyvin pienellä lisämuistinkulutuksella (listaus X).


Listaus X. Uuden arvon sijoitus järjestettyyn binääripuuhun.
Destruktiivinen versio muokkaa alkuperäistä tietorakennetta, applikatiivinen säilyttää sen.
Toteutuskieli: Python.

Destruktiivinen:                        Applikatiivinen:

def btree_insert(btree, value):         def btree_insert(btree, value):
  if value < btree['val']:                if btree is None:
    if btree['left'] is None:               return (value, None, None)
      btree['left'] = dict(val=value,     curvalue, left, right = btree
        left = None, right=None)          if value < curvalue:
    else:                                   return (curvalue,
      btree_insert(btree['left'],             btree_insert(left, value), right)
        value)                            if value > curvalue:
  if value > btree['val']:                  return (curvalue,
    if btree['right'] is None:                left, btree_insert(right, value))
      btree['right'] = dict(val=value,    return btree
        left = None, right=None)
    else:
      btree_insert(btree['right'],
        value)

Laiskat tietorakenteet

Funktion tulos riippuu vain sen syötteistä eikä tilanteesta, jossa se suoritetaan. Niinpä funktionaalisessa ohjelmassa funktioiden suorittamista ja ylipäänsä arvojen laskemista voi viivästää, kunnes tulosta oikeasti tarvitaan. Tätä suoritusjärjestysten vapautta voi hyödyntää laiskoissa tietorakenteissa. Tällöin tietorakenteen sisältöä ei lasketa etukäteen, vaan tietorakenteeseen upotetaan funktioita, joilta voi tarvittaessa pyytää tietorakenteen varsinaisen sisällön.

Laiskat tietorakenteet voivat olla äärettömän suuria, koska niitä lasketaan vain tarpeen mukaan. Esimerkkejä laiskoista tietorakenteista ovat mm. laiskat listat eli virrat tai vaikkapa puutietorakenne, josta löytyy jokainen mahdollinen shakkipelin kulku. Monet algoritmit on esitettävissä elegantisti muunnoksina, jossa funktio lukee yhtä laiskaa tietorakennetta ja tuottaa sen perusteella toista laiskaa tietorakennetta (listaus X).[[ALAVIITE: Tietyissä ohjelmointikielissä, kuten Haskellissa, kaikki tietorakenteet ovat laiskoja.]] Laiskat tietorakenteet sopivat lukuisiin tarkoituksiin aina numeerisista menetelmistä optimointiin ja jäsentämiseen.[[ALAVIITE: Niillä voi myös faktoroida lopetusehdot erilleen iteratiivisista algoritmeista siten, että iteratiivinen algoritmi tuottaa äärettömästi tarkentuvan listan tuloksia ja listaa lukeva funktio päättää, milloin on aika lopettaa.]]


Listaus X. Luonnolliset luvut ja parilliset luvut laiskana listana.
Toteutuskieli: Python, Scheme.
Scheme sisältää valmiiksi laiskojen tietorakenteiden käsittelyä helpottavia palveluita, Python ei.

Python:                         Scheme:

def delay(fn):                  (define (numbers-from n)
  return ['delay', fn]            (delay (cons n (numbers-from (+ n 1)))))

def force(thunk):               (define naturals (numbers-from 0))
  if thunk[0] == 'delay':
    thunk[0] = 'val'            (define (double stream)
    thunk[1] = thunk[1]()         (let ((val (force stream)))
  return thunk[1]                   (delay (cons (* 2 (car val))
                                                 (double (cdr val))))))
def numbers_from(n):
  return delay(lambda:          (define evens (double naturals))
    (n, numbers_from(n+1)))

naturals = numbers_from(0)

def double(stream):
  n, rest = force(stream)
  return delay(lambda:
    (n*2, double(rest)))

evens = double(naturals)

Korkeamman asteen funktiot

Yleensä funktionaaliset ohjelmointikielet tukevat myös korkeamman asteen funktioita eli funktionaaleja sekä niiden käyttökelpoisuutta huimasti lisääviä sulkeumia (closure). Funktionaali tarkoittaa funktiota, joka ottaa syötteekseen tai palauttaa funktioita. Rekursio on usein korvattavissa sopivan funktionaalin käytöllä (listaus X). Funktionaalit soveltuvat erittäin monenlaisten toimintatapojen, tietorakenteiden ja laskentaympäristöjen abstrahointiin. [[ALAVIITE: Korkeamman asteen funktioiden käyttö ei oikeastaan ole hyödyllistä vain funktionaalisen ohjelmoinnin kannalta. Imperatiivisessa ohjelmassa koodia syöttekseen ottavista aliohjelmista on lähes yhtä paljon hyötyä kuin funktionaalisessa ohjelmassa, ja joidenkin tunnettujen imperatiivisten kielten (Tcl, Ruby ja Smalltalk) ehto- ja toistorakenteita on toteutettu tällaisilla menetelmillä.]]


Listaus X. listan summa ja peräkkäisten toistuvien elementtien poisto.
Toteutuskieli: Python.

Rekursiivinen:                  Funktionaaleihin perustuva:

def summa(lista):               def summa(lista):
  if len(lista) == 0:             return reduce(lambda x,y: x+y,
    return 0                                    lista, 0)
  return lista[0] + \
    summa(lista[1:])            def toistoton(lista):
                                  return map(lambda (x,y): x,
def toistoton(lista):              filter(lambda (x,y): x != y,
  if len(lista) <= 1:                zip(lista, lista[1:]))) + lista[-1:]
    return lista
  if lista[0] == lista[1]:
    return toistoton(lista[1:])
  return lista[0] + toistoton(lista[1:])

Monessa tilanteessa funktionaalien käyttö on vaihtoehto olio-ohjelmoinnille. Olio-ohjelmoinnissahan pystyy kirjoittamaan tietorakenneriippumattomia algoritmeja käyttämällä tietorakenteiden tarjoamia metodeja. Funktionaalit taas saavat argumentteina ne funktiot (aliohjelmat), joilla tietorakennetta käsitellään. Molemmilla tavoilla saadaan aikaiseksi tietotyyppien suhteen yleistettyä koodia (listaus X).


Listaus X. onko lista järjestyksessä?
Alkioita vertaillaan metodilla tai argumenttina saadulla funktiolla.
Toteutuskieli: Python.

Metodin avulla:                         Argumentin avulla:

def is_ordered(ls):                     def is_ordered(ls, is_less):
  for e1, e2 in zip(ls, ls[1:]):          for e1, e2 in zip(ls, ls[1:]):
    if not e1.__lt__(e2):                   if not is_less(e1, e2):
      return False                            return False
  return True                             return True

Tietovuo-ohjelmointi

90-luvulta lähtien funktionaalisessa ohjelmoinnissa on yleistynyt tyyli, jossa funktiot määritellään yhdistelemällä funktionaaleilla muita funktioita. Erona perinteisempään funktionaaliseen ohjelmointiin on se, että arvojen kuljetus funktiolta toiselle on annettu funktionaalien tehtäväksi eikä arvoihin viitata enää niin paljon erillisillä muuttujilla. Erityisen keskeinen funktionaali tässä ohjelmointitavassa on kompositio-operaattori, joka antaa yhden funktion tuloksen toiselle syötteeksi.[[ALAVIITE: Muita tärkeitä funktionaaleja ovat monadisen ohjelmoinnin bind-toteutukset sekä tietorakenteita läpikäyvät katamorfismit, kuten ylempänäkin mainittu listojen reduce().]]

Monet funktiot pystyy kirjoittamaan kokonaan kompositio-operaattorin avulla. Tuloksena olevaa ohjelmointityyliä kutsutaan nimellä point-free notation ja se tuo usein selkeästi esiin tietovuot, joiden kautta arvot kulkevat laskennassa (listaus X ja X). Koska kaikki funktiot pystyy määrittelemään yksinkertaisempien funktioiden kompositioina, konkatenatiivisissa ohjelmointikielissä kompositiosta tehdään ohjelmoinnin perusoperaatio.[[ALAVIITE: Tunnetuin näistä kielistä on Factor. Monelle tietovoita korostava point-free notation on tutumpi Unix-komentoriviympäristön putkirakenteista. Esimerkiksi tämä komento laskee syötteensä sanojen frekvenssijakauman:

tr ' ' \\012 | sort | uniq -c | sort -nr
]]


Listaus X. Parillisten laskeminen.
Toteutuskieli: Haskell.

Rekursiivinen:                  Tietorakennefunktionaaleilla:
                                countEvens xs = length (filter even xs)
countEvens [] = 0
countEvens (x:xs)               Kompositio-operaattorilla:
  | even x = 1 + countEvens xs  countEvens = length . filter even
  | otherwise = countEvens xs


Listaus X. Vektoreiden pistetulo (vektorit mallinnetaan listoina).
Toteutuskieli: Haskell.

Rekursiivinen:                  Tietorakennefunktionaaleilla:

dotp [] [] = 0                  dotp v1 v2 =
dotp [] v = error "dim"          sum (zipWith (*) v1 v2)
dotp v [] = error "dim"
dotp (c1:v1) (c2:v2) =          Kompositio-operaattorilla:
  c1 * c2 + dotp v1 v2          dotp = (sum .) . zipWith (*)

Tietovuo-ohjelmointi tuottaa kaunista ja selkeää koodia silloin, kun funktioiden kompositiorakenne ei ole aivan tavattoman monimutkainen. Kun funktioiden välinen verkko monimutkaistuu, tarvitaan myös koko ajan lisää funktionaaleja ja muita apukeinoja oikeiden syötteiden ohjaamiseksi oikeille funktioille.

Logiikkaparadigma

Logiikkaparadigmassa laskentaa tarkastellaan prosessina, jossa etsitään ehtoja, joilla annetut väitteet pitävät paikkansa. Esimerkiksi väite [1, X, 3] = [1, 2, Y] pitää paikkansa silloin ja vain silloin, kun X = 2 ja Y = 3. Ohjelmia muodostetaan logiikkaparadigmassa määrittämällä uusia predikaatteja. Predikaatti määrittää, missä suhteessa sen argumentit ovat toisiinsa.

Monimutkaiset predikaatit määritellään yleensä sen perusteella, miten ne riippuvat yksinkertaisemmista (listaus X). Laskenta etenee siten, että etsitään logiikkalausekkeita seuraamalla ne muuttujien arvot, joilla kaikki ohjelman väitteet pitävät yhtaikaa paikkansa. Logiikkaohjelmointi soveltuu erityisen hyvin tilanteisiin, joissa etsitään ratkaisua monimutkaiseen rajoiteongelmaan (esimerkiksi liikennevalojen ajastukseen), haetaan erilaisista vaihtoehdoista parasta (esimerkiksi shakkitekoäly) tai parseroidaan eli jäsennetään kieliä.


Listaus X. listan pituus ja kahden listan yhdistäminen.
Toteutuskieli: Prolog.

Listan pituus:                  Listojen katenaatio:

length([], 0).                  cat([], L2, L2).
length([F|Rest], X) :-          cat([E1|L1], L2, [E1|L3]) :-
  length(Rest, Y),                cat(L1, L2, L3).
  plus(Y, 1, X).
                                Käyttöesimerkit:
Käyttöesimerkit:
                                ?- cat([1,2], [3,4], X).
?- len([4,6,2], X).             X = [1, 2, 3, 4].
X = 3 .                         ?- cat(X, Y, [a, b]).
?- len([1,2|X], 4).             X = [],
X = [_G335, _G338] .            Y = [a, b] ;
?- length([1,X,3], 2).          X = [a],
false.                          Y = [b] ;
                                X = [a, b],
                                Y = [] .

Listauksen käyttöesimerkeistä näkyy, että määritelmien perusteella voi laskea pituuksia listoista, listoja pituuksista, yhdistettyjä listoja osalistoista tai osalistoja yhdistettävistä listoista. Lisäksi ratkaisuja on joskus yksi, joskus monta ja joskus nolla.

Sekä logiikka- että funktionaalinen paradigma ovat esimerkkejä deklaratiivisesta ohjelmoinnista. Deklaratiiviset ohjelmat eivät ole vain ohjeita, miten jotain lasketaan, vaan ne voi tulkita myös väitteiksi, miten asiat ovat. Funktioiden määrittelyt ovat käytännössä väitteitä siitä, mikä on funktion syötteiden ja tuloksen suhde. Esimerkiksi tämä Haskell-ohjelma kertoo, mikä on listan ja sen pituuden suhde.

length [] = 0
length (x:xs) = 1 + length xs

Logiikkaohjelmointi menee tässä pidemmälle ja tekee ohjelman predikaateista symmetrisia siten, että niissä ei määritetä, mikä on syötettä ja mikä tulos, vaan väitteen yhden argumentin voi laskea toisen perusteella tai päin vastoin.

Indeterminismi

Kuten todettu, logiikkaohjelmassa asetettuihin ehtoihin voi olla yksi, monta tai nolla ratkaisua. Joskus ohjelmaa suoritettaessa tiettyyn osaongelmaan on monta ratkaisua mutta jokin niistä ei myöhempien sääntöjen vuoksi kelpaakaan. Tällöin joudutaan palaamaan takaisin (backtrack) muihin mahdollisiin ratkaisuihin ja katsomaan, tyydyttäisikö seuraava niistä loputkin rajoitteet. Tällaista suoritustapaa, jossa mahdollisia tuloksia on useita ja tutkitaan läpi kaikki mahdollisuudet, kutsutaan indeterministiseksi (listaus X). Indeterminismi ei ole ainoastaan logiikkaohjelmointiin rajoittuva tekniikka, vaan myös funktionaalisessa ohjelmoinnissa voi toteuttaa funktioita, jotka palauttavat haluttaessa useampia arvoja (tai ei yhtään).[[ALAVIITE: Puhtaissa funktionaalisissa kielissä indeterministinen funktio yleensä palauttaa laiskan listan erilaisista tuloksistaan, mutta jatkeita (continuations) tukevissa ohjelmointikielissä indeterminismin voi toteuttaa myös hyppimällä jatkeilla ohjelman suorituksessa takaisin aikaisempiin vaiheisiin.]]


Listaus X. osaongelma, jolla on monta ratkaisua ja lisärajoite.
Toteutuskieli: Prolog.

X on jokin listan jäsen:        Lisärajoite X > 2:

?- member(X, [1, 3, 5]).        ?- member(X, [1, 3, 5]), X > 2.
X = 1 ;                         X = 3 ;
X = 3 ;                         X = 5.
X = 5.

Oppia ikä kaikki

Tässä jutussa on käsitelty vain pintapuolisesti kaikkein yleisimpiä paradigmoja, joiden avulla ohjelman toimintatavan voi määritellä. Jokainen tässä esitellyistä ohjelmointiparadigmoista kantaa mukanaan vuosikymmenten perinnettä ja tutkimusta siitä, miten erilaiset ongelmat voi ratkaista elegantisti, ja lisäksi on olemassa uudempia paradigmoja, joiden tutkimus on vasta alullaan. Jo vähäinenkin ohjelmointitapojen tuntemus kehittää ohjelmoijan taitoja, mutta erilaisten ohjelmointiparadigmojen tarjoamat uudet ajatukset ja välineet muodostavat lähes ehtymättömän aarreaitan kiinnostuneille.

Toivon, että tämä kirjoitus rohkaisi sinua tutustumaan uusiin ohjelmointitapohin. Ohjelmointi on ajattelua, joten uudet ohjelmointitavat ovat samalla uusia ajatustapoja ja auttavat parhaimmillaan ymmärtämään myös maailmaa paremmin.


vim:et:ts=8:


Pikalinkit:


kommentoi (viimeksi muutettu 04.10.2013 15:20)