tiistai 27. tammikuuta 2009

Demo 2 - kommentit

Toisen demokerran tehtävät löytyvät osoitteesta http://users.jyu.fi/~antkaij/opetus/okp/2009/demot/2.html . Tämänkertaiset tehtävät eivät olleet yhtä työläitä kuin ensimmäisen.

Ensimmäinen tehtävä oli lähinnä käsitteiden kertaamista eikä sinänsä tarjonnut yllätyksiä.

Toisen tehtävän Duff's devicesta olen kuullut joskus aiemminkin. Kuitenkin on aika selvää, että moisia häkkyröitä ei toivottavasti ole enää tänä päivänä tarpeen käyttää (ainakaan sovellustason ohjelmoijan :) ).

Kolmanteen tehtävään ongin selville mitä muistinhallintatekniikoita Lua, Python ja Ruby käyttävät. Python luottaa viittausten laskentaan (reference counting) ja erityiseen muistinsiivoajaan, joka huolehtii syklisistä tapauksista, joita viittausten määrää laskemalla ei havaita. Ruby käyttää merkkaa ja lakaise (mark and sweep) metodia kenties hieman yllättäen. Lua käytti kyseistä tapaa aiemmin mutta on sittemmin siirtynyt käyttämään inkrementaalista järjestelmää. Kyseinen järjestelmä on esimerkiksi latenssin (esim. pelikäyttö) kannalta parempi kuin edeltäjänsä.

Neljättä tehtävää en merkinnyt tehdyksi, mutta luin Dijkstran artikkelin ja vilkaisin Knuthin pakettia. Sinänsä "Go to considered harmful" oli hieman laimea esitys, mutta tämä johtui sen saamasta hypetyksestä. Sinänsä artikkeli sisälsi kuitenkin ihan asiallista tekstiä. Kokonaisuutena tuntui, että molemmat artikkelit olivat ajalta jolloin ohjelmoijat, tai ainakin osa heistä, oli vielä henkisesti kiinni "goto" maailmassa ja muutos parempaan oli paraikaa tapahtumassa.

Mielestäni goto sinänsä on aivan liian matalan tason työkalu korkean tason kieliin. Voikin olla parempi, että sen käyttö onkin sallittua vain sopivan matalan tason kielissä, joissa sen käyttö on hyvin perusteltua esimerkiksi optimointisyin. Goto on yksinkertaisesti liian voimakas sellaisenaan. En väitä etteikö gotoa voisi käyttää "oikein", mutta sen käyttäminen väärin saattaa olla houkuttelevaa. Tämä lienee eräs syy siihen, miksi esim. C++ -kieltä pidetään hankalana. Kieli sinänsä on erittäin pätevä työkalu, oikeissa käsissä. Sama pätee gotoon.

Tällä kertaa en tehnyt bonustehtävää. Kenties ensi kerralla. :)

Luento 6 - Denotaatio

Luennon 6 aiheena oli jo aiemmin sivuttu denotaatio. Denotaation ajatuksenahan on ohjelmointikielen esittäminen tietokoneesta riippumattomalla tavalla. Denotationaalinen tapa esittää ohjelmointikieli on hyvinkin matemaattinen. Matematiikan avulla esitystavasta saadaan eksakti ja tietyllä tavalla universaali. Luennolla painottui erityisesti matematiikan merkitys abstrahoinnin apuvälineenä. Denotaatiossa matemaattisen notaation tarkoitushan on yksinkertaistaa asioita sen sijaan että se tekisi niitä monimutkaisemmiksi kuin ne ovat.

Matemaattisen notaation käyttämisen hyödyistä mainittiin myös sen vaativuuden hyöty. Käytännössä se pakottaa miettimään asiat tarkemmalla tasolla kuin muuten kenties tulisi tehneeksi. Tässä mielessä lähestymistapa muistuttaa testaamisesta. Toisaalta voisi kuvitella, että kielten määritelmille voitaisiin niillekin antaa määritelmiä (lue testejä), joiden avulla pyrittäisiin takaamaan määritelmien jonkinmoinen oikeellisuus. Toisin sanoen kielen määritelmän voisi rajata jo sen määrittelyn avulla. Tiedä sitten tehdäänkö käytännössä näin.

Ohjelmointikielten suunnittelun tapauksessa formaali tapa lähestyä asioita on erittäin arvokasta. Tällä tavoin kieli voidaan todistaa toimivaksi jo ennen kuin sille tehdään toteutus. Ilman formaalia lähestymistapaa kieleen saattaa jäädä pieniä semanttisia "porsaanreikiä", jotka sitten myöhemmin siunataan ominaisuuksiksi, koska niitä on totuttu jo käyttämään.

Sen sijaan, että ikävystyttäisin lukijaa luennolla käytyjen esimerkkien tarkalla kertaamisella, tällä kertaa keskitynkin esimerkeissä esiintyneisiin avainasioihin ja niiden pohtimiseen kuitenkin seuraten niiden esiintymisjärjestystä. Toisaalta blogisofta antaa hieman huonosti myöden matemaattiselle notaatiolle. :)

Tietyssä mielessä denotationaalinen ohjelmointikielen määrittely on ohjelmointia sinänsä. Tässä tapauksessa ohjelmointikielenä on vain matematiikka. Oleellista määrittelyissä on nimenomaan kielen semantiikan määrittely. Määrittelyt, jotka eivät kuvaa semantiikkaa eivät ole hyödyllisiä, kuten luennolla näimme. Tähän palaan tuonnempana.

Luennolla kerrattiin erityissulkeiden (esim. E[[e]] merkitys). Ajatuksena on, että näiden semanttisten sulkeiden avulla voidaan erottaa metakieli todellisesta kielestä. Toisin sanoen notaation avulla voidaan kuvata, kuinka esimerkiksi tietty matemaattinen operaatio kuvautuu ohjelmointikieleen. Mielestäni on tärkeää muistaa, että tämä kuvaus ei ole aina identtinen. Toisin sanoen voi olla luontevaa, että metakielen '+' kuvautuu todellisessa kielessä '+' merkiksi, mutta esimerkiksi sanarikasta suomenkielistä ohjelmointikieltä laatiessa se voisi olla vaikka 'plus' (viisi plus kuusi) tai 'summaa' (summaa viisi ja kuusi). Käytännöllisempi esimerkki on tietenkin ohjelmointikielten käyttämä notaatio potenssin merkinnälle. Tavallisimpia tässä tapauksessa ovat ^ ja **.

Mikäli kielessä halutaan määritellä muuttujia, voidaan apuvälineenä käyttää ns. "currying" menetelmää. Muuttujien määrittely ei sinänsä ole mikään helppo temppu. Semanttisesti se voi tarkoittaa, että tiettyyn olioon assosioidaan toisen olion sisältämä tieto. Perinteisesti tämä tehdään vielä siten, että sijoitus tehdään lausekkeen vasemmalle puolelle siten, että olio, jonka tieto vasemmalla puolella olevaan olioon liitetään, on oikealla. Tätä ajatustapaa voi yleistää myös sijoitukseen, jossa useaan muuttujaan sijoitetaan tietoa samanaikaisesti tietyn ehdon mukaisesti (esim. "a, b, c = 3, 4, 6"). Käsittääkseni "currying" pyrkii ratkaisemaan juuri näitä ongelmia ja sinällään tarjoamaan omanlaisensa abstraktion apuvälineen. Mielenkiintoisesti kielten määrittelyissä käytetään apuvälineenä metamuuttujia, joiden avulla kuvataan muuttujan semantiikkaa.

Suoraviivaohjelmien tapauksessa esiteltiin ympäristön käsite. Aluksi notaatio ja ympäristön tarve tuntuivat oudoilta, mutta luennolla jaettua monistetta lainaten "Kun edellä muuttujien arvo määräytyy ympäristön (Env) perusteella, on lauseen muutettava ympäristöä." Toisin sanoen ympäristön tarve tulee suoraan suoraviivaohjelmien määritelmästä mikä tuntuu sinänsä luontevalta.

Kuten jossain aiemmassa blogipostissa olen varmaan maininnutkin, suoraviivaohjelmat ovat esiehto "oikeille" ohjelmille, joissa on myös ehtolauseita ja luuppeja. Ehtolause on joko tosi tai epätosi. Tietenkin tämä heijastuu myös itse määritelmään. Luuppien tapaus on ehtolauseita kinkkisempi. Ongelmaksi muodostuu nimenomaan se, kuinka luuppi kannattaisi määritellä. Syklinen, luupin itsensä avulla kuvaava määrittely ei kerro sen semantiikasta mitään. Tämän vuoksi esitettiin erityinen pohjan käsite, johon luupin käsite voidaan sitoa. Luupin tapauksessa luupilla voi olla konkreettinen pohja tai ei (eli luuppi ei pääty koskaan, jolloin se ei saavuta pohjaansa. Tämä ei tarkoita etteikö luuppia voitaisi ajatella pohjan käsitteen kautta.).

Äkkiseltään denotationaalinen kielen määrittely saattaa näyttää kryptiselta. Kuitenkin noiden määrittelyjen pienten palojen ymmärtäminen auttoi ymmärtämään sitä, mistä niissä pohjimmiltaan on kysymys, hieman paremmin. Loppujen lopuksi kaikki palautuu määritelmiin ja matemaattisiin työkaluihin, joiden tärkeimpänä tehtävänä on tehdä asioista mahdollisimman ymmärrettäviä, matemaattisessa mielessä (em. tarkoittaa myös sitä, että notaation ja käsitteiden tulee olla tuttua). Kurssin suurimpia anteja lienee nimenomaan se, että määritelmiä oppii ainakin jossain määrin ymmärtämään.

maanantai 26. tammikuuta 2009

Luento 5 - Aliohjelmat

Luennon aluksi esitettiin väittämä, että eri ohjelmointikielellä tehdyt ohjelmat sisältävät saman määrän bugeja suhteutettuna tuotettujen koodirivien määrään. Väittämän mukaan toisin sanoen esimerkiksi C:llä kirjoitettu, ilmaisultaan sanarikkaampi koodi sisältäisi saman määrän bugeja kuin vaikka Pythonilla kirjoitettu korkean tason koodi, jossa ei oteta kantaa esimerkiksi muistinhallintaan lainkaan. Sinänsä väittämä tuntuu intuitiivisesti todelta.

Huomaa, että väittämä ei sano mitään bugien vakavuudesta tai niiden korjattavuudesta. Se koskee vain bugien määrää. Uskonkin, että korkeamman tason kielellä kirjoitettu koodi onkin helpommin korjattavissa kuin alemman tason kielellä kirjoitettu. Toisaalta tehdyt virheet eivät välttämättä ole yhtä vakavia kuin alemman tason kielen tapauksessa. Tämä johtuu siitä, että riskialttiisiin seikkoihin on jo otettu kehittäjän puolesta kantaa. En sano etteikö matalan tason kieliä kannattaisi käyttää, jos käyttö on perusteltua. Totean vain, että korkean tason kielten tapauksessa virheet ovat erityyppisiä ja uskoakseni helpommin sekä nopeammin korjattavissa.

Luennon alussa oli mainintaa myös kirjastoista ja niiden käytöistä. Mielenkiintoisin huomio oli se, että ei ole mielekästä kirjoittaa kirjastoa vain sen takia, että se voidaan kirjoittaa. Kirjastolla tulee olla selvä käyttötarkoitus. Muulloin työ on saattanut olla jossain määrin turhaa. Toisaalta käytetyn kirjaston eri käyttäjäryhmät saattavat esittää keskenään jopa ristiriitaisia vaatimuksia. Tämä voikin olla eräs syy siihen, miksi kirjastojen käyttämistä sellaisenaan saatetaan karsastaa. Parhaimmillaan kirjaston käyttäminen on synergistinen prosessi, jossa sekä sen käyttäjä että kehittäjä hyötyvät. Kehittäjät saavat palautetta työstään. Toisaalta käyttäjät saavat tarvitsemiaan parannuksia. Ainakin näin karkeasti ajatellen. Käytännössä tilanne on paljon monitahoisempi.

Luennon varsinainen aiheena olivat aliohjelmat. Miksi aliohjelmia ylipäänsä tarvitaan? Luonteva vastaus lienee toiston poisto. Sen sijaan, että koodinpätkää kopioisi joka paikkaan, voi olla mielekästä määritellä se aliohjelmana. Tätä aliohjelmaa voitaisiin sitten kutsua eri paikoista koodia aina tarpeen mukaan. Kutsu tehdään hyödyntäen apuna itse aliohjelman määrittelyä. Määrittelyssä voidaan kertoa mitä parametreja aliohjelmalle annetaan. Toisaalta määrittelystä saatetaan nähdä aliohjelman palautustyyppi.

Ei ole mitenkään itsestään selvää millainen aliohjelman määrittelyn tulisi olla. Joissain kielissä määrittely voi olla erittäin vapaa. Esimerkiksi palautustyyppiä ei välttämättä tarvitse määritellä ollenkaan vaan se määräytyy implisiittisesti aina palautetun tyypin mukaan. Näin siis dynaamisissa kielissä.

On myös mahdollista, että parametreja ei rajata ollenkaan. Aliohjelma voidaan siis toisin sanoen antaa mitä parametreja ikinä halutaan. Missä tapauksessa tällaisesta vapaasta määrittelystä voisi olla sitten hyötyä? Itse olen käyttänyt kyseistä tapaa esimerkiksi dynaamisten hakujen suorittamiseen. Ajatuksena on, että aliohjelma saa nimetyn parametrin (esim. joukko.tee_haku(nimi='Jeppe')), joka kertoo haetun attribuutin nimen sekä siihen liittyvän haetun arvon. Hieman lisää miettimällä löytää varmasti muitakin käteviä esimerkkejä. Mielestäni on kuitenkin oleellista ymmärtää, että aliohjelman konkreettinen määritelmä voi olla hyvinkin erilainen aina kielestä riippuen.

Luennolla esiteltiin, kuinka aliohjelmat voivat toimia muistinhallinnan suhteen. Oleellisena esille nousi stack framen (pinokehys?) käsite. Tavallisesti aliohjelmakutsujen voidaan ajatella luovan uusi pinokehys kutakin kutsua varten. Kehys sisältää viitteet kutsujaan, parametreihin, palautusarvoihin sekä paikallisiin muuttujiin. Käsittääkseni tämä kutsu tunnetaan myös jatkeen nimellä (ts. aliohjelmien kutsupinoon lisätään uusi kehys). On olemassa tapaus, jossa aliohjelmakutsua varten ei tarvitse luoda uutta kehystä.

Kyseessä on häntäkutsun poisto. Toisin sanoen jos aliohjelmaa itseään kutsutaan sopivasti, voidaan sen parametrit syöttää sen nykyiseen pinokehykseen. Tällä tavoin suoritus pysyy paikallaan muistiavaruudessa eikä ylimääräistä pinomuistia tarvita. Luennolla osoittautui lisäksi, että tämänkaltainen rekursio vastaa while-luuppia, mikä sinänsä on varsin luontevaa. On huomattava, että kyseinen optimointi tulee toteuttaa ohjelmointikielen tasolla eikä tule olettaa, että se toimisi kaikissa kielissä. Häntäkutsun poiston ja while-ekvivalenssin ansiosta rekursio voi parhaimmillaan olla yhtä tehokas ratkaisu, mikä saattoi olla kenties hieman yllättävää.

Luennolla mainittiin myös sulkeumista (closure), jotka olivatkin käsitteenä jo tuttu. Perusajatuksena on, että aliohjelmien sisälle voidaan määritellä uusia aliohjelmia. Tavallisesti nämä sisäänmääritellyt aliohjelmat näkevät vanhempansa muuttujat, mistä sinänsä voi olla mielenkiintoista hyötyä joskus. Ohjelmointikielen toteutuksen kannalta tämä tarkoittaa, että aliohjelman sisällä olevaa aliohjelmaa kutsuttaessa sille tulee attribuuttien (huom! attribuutit mappautuvat aliohjelman parametreihin kutsussa) lisäksi viedä viite sen vanhempialiohjelmaan, jotta aliohjelman aliohjelma pääsee käsiksi sen muuttujiin edellä mainitulla tavalla. Tämä tehdään siis ohjelmoijalta piilossa.

Parametreihin liittyen esitettiin erityisiä välitysmuotoja, jotka olivat in (luku), out (kirjoitus) sekä inout (luku/kirjoitus). Näistä in-tyyppiset ovat varmasti tutuimpia. Ne ovat juurikin niitä parametreja, jotka aliohjelmalle annetaan lähtöarvoina, joiden mukaan operoida. out-vaihtoehtoa voidaan pitää jotakuinkin tuloarvon tai aliohjelman palautusarvon eräänlaisena vastineena ja inoutia edellä mainittujen yhdistelmänä. Lisäksi esitettiin parametrinvälitysmekanismeja, joista erityisesti otti silmään call-by-name, joka muistutti jossain määrin template method (mallinemetodi???) suunnittelumallista, joka on oikein pätevä yleiskäyttöistä koodia kirjoittaessa. call-by-namessa ajatuksena on, että parametrin nimi korvataan kaikkialla aliohjelman sisällä annetulla lausekkeella (esim. a:n sijalle voidaan sijoittaa x=x+1). Template methodia käyttäen tekee helposti esim. listan järjestelijän (vertailija on aliohjelma, joka annetaan järjestäjä aliohjelmalle). Sama idea saattaisi toimia jopa call-by-namen tapauksessa.

Luennon lopuksi esiteltiin vielä lyhyesti vuorottaisrutiinit (coroutines), jotka olivatkin jo ennalta tuttuja Luasta. Myös Pythonin samantyyliset generaattorit ovat tuttuja. Pohjimmiltaan ajatus on se, että aliohjelma muistaa tilansa tai tarkemmin viimeisimmän suorituskohtansa ennen siitä poistumista. Vuorottaisrutiineista on erityistä hyötyä toteuttaessa esimerkiksi tuottaja-kuluttaja -malliin pohjautuvia algoritmeja. Hyvä esimerkki tästä löytyy osoitteesta http://www.lua.org/pil/9.2.html.

Itse näen aliohjelmat erityisesti abstraktion apuvälineenä. Hyvin nimetyt ja oikein koostetut aliohjelmat auttavat ymmärtämään, mistä koodissa on kyse. Logo-kielessä (Turtle graphics!) aliohjelman merkityksestä kertoo niiden määrittely "TO" sanan avulla. Ts. aliohjelma voidaan kuvata tyyliin "TO BAKEACAKE", joka jostain kumman syystä tuntuu luontevammalta kuin esim. Pythonin "def bake_a_cake()". Itse aliohjelman kuvaus on sitten vain "ruokaohje", mitä sen hyvinkin pitkälle aina jossain määrin tulisi olla.

Aliohjelmien merkitystä ohjelmointikielten kehityksessä ei tule väheksyä. Luento auttoi ymmärtämään hieman paremmin, mistä niissä on pohjimmiltaan kysymys ja miten niitä ainakin teoriassa voitaisiin varioida.

keskiviikko 21. tammikuuta 2009

Demo 1 - kommentit

Ajattelin tässä samalla kirjoittaa hieman kommentteja demoista. Eipä tuosta liene haittaakaan ole. Mikäli lukijaa sattuu kiinnostamaan itse tehtävät, ne löytyvät osoitteesta http://users.jyu.fi/~antkaij/opetus/okp/2009/demot/1.html . Kertaan kuitenkin kommentoimani tehtävät tässä, jotta ei tarvitse pitää kahta sivua samaan aikaan auki ja vilkuilla edestakaisin. Joten sitten kommentteihin ja omiin ratkaisuihin...

Tehtävä 1 alkoi lupaavasti seuraavalla kuvauksella "Kommentoi Java-kielen operaattoreiden presedenssijärjestystä. Onko siinä korjaamisen varaa ja jos on, minkälaista? Perustele.". En ole mikään Java-guru, todettakoon se nyt alkuun. Kuitenkin kuuklettamalla löytyi seuraava sivu: http://www.uni-bonn.de/~manfear/javaoperators.php . Aluksi touhu meni hieman ihmettelyn puolelle. Kuitenkin jossain vaiheessa välähti ja rupesin ihmettelemään boolean operaattoreiden ja yhtäsuuruusvertailujen välistä järjestystä. Tämä sattuikin sitten olemaan olennaisin havainto.

Kenties hieman yllättäen Javassa lauseke "A && B == C || D" arvioituu seuraavalla tavalla: "A && (B == C) || D". Tämä ei ole lähelläkään sitä, mitä voisi olettaa tapahtuvan eli toisin sanoen voisi luulla, että loogista sen sijaan olisi seuraava tulos: "(A && B) == (C || D)". Luennolla selvisi, että tämä ihmeellinen, harmaita hiuksia kasvattava tapa, periytyy vanhasta kunnon C:stä (näköjään päässyt hieman unohtumaan :) ). Joten mikäli Javassa (tai C:ssä) haluaa käyttää booleaneja ja yhtäsuuruusvertailua samassa lauseessa, kannattaa suluttaa itse.

Toinen tekemäni huomio liittyi >>> operaatioon, joka myös nimellä "unsigned bit shift right" tunnetaan. Hieman asiaa tongittuani näyttää siltä, että kyseessä on operaatio, joka vain hävittää etumerkin shiftaamisen lisäksi. Syy miksi operaatiota ei löydy toisinpäin (eli <<<) on luonnollinen. Luvullahan on etumerkki vain toisessa päässä (ellei käytä jotain ihmeellistä lukujärjestelmää). Toisessa tehtävässä oli seuraava tehtävänanto: "Hahmottele merkkijonojen käsittelyyn sopivien lausekkeiden abstrakti syntaksi.". Tehtävässä lähdin liikkeelle yksinkertaisista vaatimuksista. Eli tarkoituksena oli luoda syntaksi, jossa voi yhdistää sekä monistaa merkkijonoja sekä poimia tietyn tai tietyt merkit merkkijonosta annetun tai annettujen indeksien (alkaa nollasta) avulla. Eräs jossain määrin järkevä tapa tehdä tämä voisi olla seuraava:


E := C, missä C on jokin merkki tai kokonainen merkkijono
| E + E # tämä yhdistää merkkijonoja
| N * E # tämä monistaa merkkijonoa N (N e N (kuvittele tähän luonnollisia lukuja edustava hieno N)) kertaa
| C[N] # palauttaa tietyn merkin
| E[N:] # merkit N:stä eteenpäin
| E[:N] # merkit ennen N:ää
| E[N:N] # em. yhdistelmä Alun perin viimeisissä oli C E:n sijalla.


Osoittautui kuitenkin, että on käytännöllisempää hyödyntää yleisempää E määrittelyä, jolloin operaatioita voidaan linkittää vapaammin. Kolmas ja neljäs tehtävä olivat seuraavia: 3. Kirjoita ohjelma, joka ottaa syötteenä tavallisen aritmeettisen lausekkeen (infix-muodossa) ja tulostaa sen postfix-muodossa. 4. Kirjoita ohjelma, joka ottaa syötteenä postfix-muotoisen aritmeettisen lausekkeen ja laskee lausekkeen arvon. Pidä huoli, että ohjelmasi kykenee lukemaan edellisen tehtävän ohjelman tuotosta. Lähdin ratkaisemaan kumpaakin tehtävää yleisen abstraktiin syntaksipuuhun pohjautuvan ajatuksen kautta. En kuitenkaan saanut ratkaisuani aivan valmiiksi. :) Sinällään melko yksinkertaiset tehtävät osoittautuivat yllättävän työläiksi. Yleisellä tasolla ratkaisuni arkkitehtuuri oli seuraava:
  • Tehdasluokka abstraktien syntaksipuiden luomista varten. Tehdas voi luoda syntaksipuun esimerkiksi infix tai postfix -syötteen pohjalta.
  • Varsinainen abstraksi syntaksipuu. Kyseessä on binääripuu ja se sisältää viitteen juureensa. Lisäksi puuhun voidaan tehdä erinäisiä hakuja sekä sitä voidaan selata halutussa järjestyksessä.
  • Syntaksipuun solmu. Solmu sisältää mahdollisen viitteen vanhempaansa (juurella ei ole vanhempaa) sekä lapsiinsa (vasen ja oikea).
Pythonista löytyy luonnollisestikin toteutus abstrakteille syntaksipuille (http://docs.python.org/library/ast.html). Tuota käyttämällä olisi kuitenkin päässyt jo turhan helpolla. :)

Bonustehtävän tehtävänanto oli seuraava: "Kirjoita ohjelma, joka ottaa syötteenä aritmeettisen lausekkeen ja tuottaa ulos Javan .class-tiedoston, jossa on main-metodi, jonka suorittaminen laskee kyseisen lausekkeen arvon. Voit käyttää apuna esim. BCEL-kirjastoa.".

En ollut aiemmin käyttänyt BCELiä. Käytännössä ratkaisuni oli tyyliä "mennään sieltä mistä aita on matalin" eli periaatteessa oikein, mutta käytännössä väärin. :) Ohjelmani hoiti syötteen ottamisen ja .class-tiedoston tuottamisen oikein. Se, missä menin noin ajatuksen tasolla metsään oli lausekkeen arvon laskeminen. Hoidin laskennan jo varsinaisessa ohjelmassa hyödyntäen erästä Eval-kirjastoa (https://eval.dev.java.net/), koska en halunnut/jaksanut omaa kirjoittaa. Oikeampi ratkaisu olisi tehdä tämä hyödyntäen apuna suoraan BCELiä ja suorittaa lasku pinon avulla (tätä varten tehtävät 3. ja 4. olisi ehkä kannattanut kirjoittaa Javalla).

Mielenkiintoisena kuriositeettina ratkaistakseni bonustehtävän kokeilin ihan huvikseni Jythonia. Epäonnekseni törmäsin kuitenkin vaikeuksiin (mappaus Javan ja Pythonin funktioiden välillä ei liene aina täydellinen?), jotka ajoivat minut takaisin Javan pariin. Kuitenkin voi olla, että mikäli jälleen tarvitsee jotain Javalla tehdä, testailen sitä hieman lisää. Ainakin tällä hetkellä suhtaudun siihen varovaisen positiivisesti. :)

Luento 4 - Kontrollivuon ohjaus

Neljännen luennon aiheena oli kontrollivuon ohjaus. Osin luento täydensi myös edellistä käyden läpi muistinsiivoukseen liittyvää ongelmatiikkaa. Useat tuntevat muistinsiivouksen myös termillä roskienkeruu. Mielestäni muistinsiivous toimii terminä ehkä aavistuksen paremmin sen korkeamman abstraktiotason vuoksi.

Miksi muistinsiivousta ylipäätään tarvitaan? Koska koneiden käytössä olevat muistiresurssit ovat rajallisia on mielekästä, että niitä käytetään mahdollisimman tehokkaasti ja muistia liialti haaskaamatta hyväksi. Ohjelmaa suoritettaessa edellisestä kirjoituksesta tutut oliot syntyvät ja aikanaan kuolevat. Muistinsiivouksen ajatuksena onkin siivota näitä kuolleita olioita pois muistista, jotta olioiden "ruumiiden" viemää tilaa voidaan hyödyntää uudelleen. Pohjimmiltaan kyse on siis vain elintilan luomisesta olioille. Tässä tapauksessahan oliot eivät luonnosta poiketen lahoa itsestään vain säilyttävät katatonisen tilansa, kunnes joku tajuaa hankkiutua niistä eroon.

Luennolla esitettiin Stroustrupin (C++:n pääkehittäjä) esittämä mielipide siitä, että kaikkia kuolleita olioita ei välttämättä tarvitse tai kannata siivota. Tarkemmin puhe oli nimenomaan pienien muistivuotojen sallimisesta. Nähdäkseni tässä ajatuksessa ydin oli siinä, että vaatimus muistivuodottomasta ohjelmistosta saattaa olla jopa liian rankka. Mikäli tästä vaatimuksesta joustaminen tekee ohjelmistojen kehittämisestä helpompaa, saattaa se olla sen arvoista. Tässä kannattaa pitää mielessä erityisesti alusta, jolle ohjelmistoa ollaan kehittämässä. Mikäli laitteistoresurssit antavat myöden, voi olla hyvinkin tehokasta joustaa hieman.

Luennolla esitettiin kolme perustekniikkaa, joiden avulla muistinsiivousta voidaan tehdä. Jotta näitä tekniikoita voi ymmärtää tulee ymmärtää, että muistia kannattaa hahmottaa graafina. Graafilla on juurisolmuja, joista viitataan varsinaisiin käytettyihin olioihin. Oliot voivat viitata vapaasti toisiinsa.

Esitetyistä tekniikoista itselleni intuitiivisin oli viittauslaskuriin pohjautuva ratkaisu. Ajatuksena on, että kuhunkin olioon liittyy luku, joka kuvaa siihen liittyvät viitteet. Lukua päivitetään viitteitä lisättäessä tai poistettaessa. Mikäli luku on nolla, voidaan olio poistaa. Viittauslaskurin ongelmana on tapaus, jossa oliot muodostavat keskenään syklisiä luuppeja. Tällöin voidaan luoda tahattomasti saarekkeita, jotka eivät liity juurisolmuihin siten, että juurisolmuista voitaisiin kulkea saarekkeisiin. Huomaa, ettei tämä tarkoita etteikö saarista voisi kulkea juurisolmuihin! Tästä seuraa, että saarekkeita ei poisteta, koska niiden viittausmäärää kuvaava luku ei luonnollisestikaan ole nolla. Kyseistä ongelmaa voisi kenties pyrkiä kiertämään. Seuraavana esitänkin alkeellisen ratkaisun, jota en tosin ole todentanut käytännön testein.

Kaikki oliot liittyvät juurisolmuihin jossain vaiheessa. Tämä seuraa tavasta, jolla olioita luodaan. Uuden olion tulee liittyä johonkin juurisolmuihin liittyvään solmuun tai juurisolmuihin itseensä. Ongelma onkin saareen liittyvän viitteen poistamisen havaitseminen. Toisaalta tämä edellyttää, että saaren käsite tulisi voida määritellä eksplisiittisesti oliograafissa.

Huomaa, että määrittelen saaren syklisenä kahden tai useamman olion muodostamana osagraafina. Saaren syntymän voi havaita, kun luodaan uusi viite ja tarkistetaan viitteen lähde- ja kohdeolion välinen suhde käymällä graafi läpi lähtien kohdeoliosta ja pyrkien löytämään lähdeolio. Mikäli lähdeolio löydetään, syntyy liitoksen myötä saari.

Jotta tästä löydöksestä voisi hyötyä, tulee saareen liittyviin olioihin kuhunkin muodostaa oma viittauslaskurinsa saariviitteitä varten. Kun saari löydetään, siirretään kunkin olion varsinaisesta viittauslaskurista yksi viite saarilaskuriin. Tästä seuraa pieni muutos tapaan, jolla "saariolioita" poistetaan. Vaikka saariolion varsinainen viitelaskuri olisikin nolla ei se tarkoita, että olio voitaisiin poistaa. Saarioliot voidaan poistaa vain ja ainoastaan jos kaikkien saareen kuuluvien olioiden varsinainen viitelaskuri on nolla. Toisaalta tulee muistaa huolehtia tilanteesta, jossa saaria tuhoutuu (ts. sykli invalidoituu) ja päivittää laskureita jälleen tällöin.

Saariolioiden käyttäminen edellyttäisi ylimääräistä laskuria, mutta toisaalta niiden avulla voitaisiin kenties parantaa viittauslaskurin perusmallin kykyä poistaa muistista kaikki turhat oliot sykliset tapaukset mukaanlukien. Lisäksi suorituskyky saattaisi kärsiä hieman ylimääräisistä graafin läpikäynneistä johtuen.

Viittauslaskurille löytyy varmasti muitakin variantteja kuin edellä esitelty. Toinen esitelty tekniikka oli ns. merkkaa ja lakaise (mark and sweep). Yllättäen tämä oli vanhin keksityistä tekniikoista. Perusajatuksena on, että oliograafi käydään juuresta lähtien läpi ja kukin vierailtu olio merkitään vierailluksi. Tämän jälkeen kaikki oliot käydään läpi siten, että näistä poistetaan ne, joita ei ole merkattu. Näin jälkikäteen ajatellen metodi on hämmästyttävän yksinkertainen.

Kolmas läpikäyty tekniikka oli ns. "pysäytä ja kopioi". Tekniikan perusajatuksena on, että koko muisti ajatellaan kahtena osana. Toista osaa vuorollaan käytetään varsinaisena muistina toisen toimiessa varalla. Tätä varaosaa käytetään, kun muistia halutaan siivota. Tällöin kukin olio vuorollaan juuresta lähtien kopioidaan tähän varaosaan. Kun olioita kopioidaan pidetään huoli myös siitä, että olioiden väliset viitteet säilyvät ennallaan. Kopioinnin jälkeen varaosasta tulee käytettävä osa ja entisestä osasta varaosa. Kopioinnin etuna on se, että samalla voidaan "defragmentoida" muistia. Tämä etu perustuu siihen, että muistia osoitetaan varaosasta olioita varten peräkkäin. Kun kaikki oliot on kopioitu osan loppu sisältää tyhjää, helposti allokoitavaa muistia.

Siihen, mitä edellä esitellyistä tekniikoista pitäisi käyttää ja milloin, en osaa vastata. Kuitenkin oli virkistävää nähdä kuinka sama asia voidaan tehdä niinkin eri tavoin. Olennaista vielä lienee mainita, että tekniikoita voidaan varioida ja pyrkiä optimoimaan eri tavoin. Jo tämä sinällään tarjoaa mielenkiintoisia pähkinöitä purtavaksi.

Tulisiko muistinsiivoustekniikoita yleensäkään käyttää, koska ne voivat hidastaa ohjelman toimintaa (90-luvun Java lienee tunnetuin esimerkki)? Omasta kokemuksestani C:n parissa voin todeta, että kannattaa, mikäli suorituskyky ja ohjelmoijan kontrolli eivät ole olennaisimpia asioita ohjelmiston kehityksessä. Muistinsiivoustekniikoiden olemassaolo tekee nykyajan ohjelmiston kehittäjän elämän asteen helpommaksi.

Muistinsiivoustekniikoiden käsittelyn jälkeen siirryttiin luennon varsinaiseen aiheeseen eli kontrollivuon ohjaukseen. Aiemmin käsitellyt suoraviivaohjelmat eivät ole kovinkaan hyödyllisiä sellaisinaan. Ne kuitenkin tarjoavat teoreettisen pohjan, jonka päälle voidaan rakentaa oikeasti käyttökelpoisia kieliä. Jotta voidaan tehdä jotain hyödyllistä, tarvitaan vähintään ehtolause (if then ) ja hyppylause (goto).

Näiden yksinkertaisten konstruktioiden avulla voidaan rakentaa nykykielistä tuttu while. while on pohjimmiltaan vain ehdollinen goto. Tärkein seikka, mikä erottaa whilen gotosta on nimenomaan se, että while on hyvin rajoittunut tapa käyttää gotoa. Syy, minkä takia goton käytöstä ei juurikaan pidetä johtuu juurikin siitä, että se on niin voimakas työkalu. Suuren voiman mukana tulee suuri vastuu. Voikin olla hyvä, että goton käyttämistä karsastetaan sen väärinkäyttömahdollisuuksien vuoksi. Toisaalta sen hyödyntäminen saattaa olla joskus houkuttelevaa ja kenties jopa käytännöllistäkin.

Kielten konstruktioita pohdittaessa voidaan havaita, että kukin konstruktio pyrkii omalla tavallaan rajoittamaan kielen käyttämistä. Hyvä esimerkki on whilen rajoitettu muoto for. for voi pakottaa, kielestä riippuen, kehittäjän tekemään kyseisestä silmukasta äärellisen. Toisaalta for kasaa monta whilea käytettäessä hajanaisissa paikoissa olevaa seikkaa samaan paikkaan. Jo tämä sinällään tekee siitä arvokkaan ja erittäin käyttökelpoisen konstruktion. Iteraattoripohjaiset (esim. for alkio in alkiot) for konstruktiot tekevätkin ohjelmoinnista helppoa. Tosin niilläkin on omat rajoituksensa.

Sen lisäksi, että konstruktiot pyrkivät rajaamaan, pyrkivät ne toisaalta myös selkeyttämään asioita. Pahimmillaan ne kuitenkin vain sekoittavat asioita. Hyvä esimerkki tästä on ehtolausekkeiden evaluointi. Esimerkiksi kielestä riippuen ei ole lainkaan selvää mitä esimerkiksi ehtolauseesta 3 > 2 > 1 seuraa. Mielestäni hyvässä toteutuksessa keskimmäistä tekijää vertailtaisiin erillisesti sen naapureihin. Tämän perusteella voitaisiin päätellä koko lauseen totuusarvo. Toki menetelmän tulisi yleistyä. Tällöin keskimmäisen tekijän sijaan otettaisiin kaikki reunimmaisten tekijöiden väliin osuvat tekijät ja kullekin tehtäisiin vertailu samaan tapaan.

Toisinaan ehtolauseista on hyötyä myös sijoituksessa. Esim. Lua ja Python sisältävät seuraavanlaiset and ja or konstruktiot:
foo = bar or baz
foo = bar and baz
Itse hyödynnän erityisesti or tapausta. Ajatuksena tässä tapauksessa on, että mikäli bar on epätosi, sijoitetaan foo muuttujaan bazin arvo. Muulloin sijoitetaan bar. and tapauksessa bazin arvo sijoitetaan foo muuttujaan vain ja ainoastaan jos myös bar on tosi.

Kolmas, Pythonissa joskus käytetty, mielenkiintoinen konstruktio on seuraavanlainen:
foo = bar if else baz
Tässä tapauksessa barin arvo sijoitetaan foohon ehdon ollessa tosi. Muulloin sijoitetaan baz. Luettavuudeltaan konstruktio ei ole mielestäni paras mahdollinen. Tämä johtuu ifin hieman oudosta paikasta. Toisaalta konstruktio voi paisua merkkimäärällisesti turhankin pitkäksi. Tällöin suosii mieluummin perinteistä if-else paria.

Luennolla esiteltiin myös ifistä vaihtoehtoinen muoto, joka tunnetaan nimellä vahtikomento. Tässä tapauksessa ajatuksena on epädeterminismin(!) salliminen ohjelmointikielessä. Toisin sanoen siitä, mitä seuraavaksi tapahtuu ei ole välttämättä takeita. Tällaisesta konstruktiosta ja ohjelmointitavasta saattaisi olla kenties hyötyä tilanteissa, joissa ajoa halutaan satunnaistaa. Toisaalta satunnaistaminen on mahdollista deterministisissäkin kielissä, joten en näe epädeterminismiä mitenkään ylivoimaisen hyvänä ominaisuutena. Mielestäni vahtikomennoissa mennään jo kielen kannalta hieman väärään suuntaan ottaen huomioon, että kielten perustehtävähän on yksinkertaistaa asioita sopivasti sen sijaan, että luotaisiin lisää kompleksisuutta. Tosin voi olla, että en ole huomannut jotain vahtikomentojen oleellista hyvää ominaisuutta, joten voi olla että niillekin löytyy käyttönsä.

Yhteenvetona todettakoon, että luennolla sain kattavamman kuvan muistinsiivoamistekniikoista sekä paremman käsityksen primitiivisistä konstruktioista (if, goto, while, ...) sekä niiden variointimahdollisuuksista. Samalla käsitykseni siitä, mitä ohjelmointikielet loppujen lopuksi ovat, vankistui hieman.

maanantai 19. tammikuuta 2009

Luento 3 - suoraviivaohjelmat

Kolmannen luennon aiheena oli suoraviivaohjelmat. Luennolla jaetun monisteen mukaan suoraviivaohjelmat voidaan määritellä ohjelmiksi, jotka eivät sisällä silmukoita tai muita hyppyoperaatioita. Nähdäkseni luento meni hieman tämän otsikon ohi (poimin otsikon aina jaetusta materiaalista), joten seuraavaa luentomuistiinpanojen purku ei välttämättä osu aina täsmälleen aihepiiriin.

Luennolla esitettiin sanapareja, jotka liittyvät toisiinsa jollain oleellisella tavalla. Ensimmäinen esitelty pari oli itsellenikin hyvin läheinen staattinen vs. dynaaminen. Staattisina voidaan pitää sellaisia asioita, jotka tiedetään jo ennalta. Dynaamiset asiat määräytyvät jo tapahtuneiden asioiden perusteella.

Ohjelmointikielten kontekstissa voidaan nähdä selvä jako dynaamisten ja staattisten kielten välillä. Olennaiset erot näiden kielten välillä syntyvät esimerkiksi tavasta määritellä muuttujia. Staattisissa kielissä muuttujan tyyppi määritellään eksplisiittisesti. Dynaamisten kielten tapauksessa tämä määräytyy itse sijoituksen perusteella implisiittisesti. Toki halutessaan ohjelmoija voi testata muuttujan tyyppiä jälkikäteen, mikäli hän näkee sen tarpeelliseksi.

Olen samaa mieltä luennoitsijan kanssa siitä, ettei voida sanoa, että toinen tapa olisi toista parempi. Itse näen, että kummallakin tavalla saa aikaan asioita. Varsinaiset erot syntyvät nähdäkseni ajattelutavassa. Staattisissa kielissä asioita (esim. em. tyypit) pyritään rajoittamaan ennalta. Dynaamisten kielten tapauksessa rajoittaminen tapahtuu vasta ohjelmoijan niin halutessa.

Oman kokemukseni perusteella uskon, että hyvät ja kattavat testit ovat oiva tuki kehittämiselle erityisesti dynaamisia kieliä käytettäessä. Niistä on hyötyä toki myös staattisissa kielissä. Niiden suurin etu on se, että ne auttavat kehittäjää nimenomaan rajoittamisprosessissa. Ohjelmointihan on jossain määrin asioiden ymmärtämistä ja mallintamista mahdollisimman luontevalla tavalla. Selkeä, asian ymmärtämistä kuvastava ajattelutapa heijastuu suoraan lähdekoodiin.

Toinen oleellinen käsitelty pari oli denotationaalinen vs. operationaalinen. Operationaalinen tarkoittaa sitä, miten ohjelma käyttäytyy ajonaikana. Ohjelmointikieli saa merkityksen sitä kautta, miten se ohjaa tietokoneen käyttäytymistä. Denotationaalinen sen sijaan tarkoittaa, että itse ohjelmointikieli on olemassa riippumatta tietokoneen olemassaolosta!

Voidaan siis puhua eräänlaisesta tietokoneriippumattomasta tavasta ajatella ohjelmointia. Oleellista on myös, että denotalionaalisuus kuvaa käytettyjen käsitteiden, esimerkiksi neliöjuuri, merkityksiä. Voisi kuvitella, että tämänlaisesta tavasta ajatella ohjelmointikieltä olisikin hyötyä esimerkiksi matemaatikoille tai kielten suunnittelijoille.

Kielten suunnittelijat käyttävätkin seuraavan parin toista osaa, metakieltä. Metakielen parina esiteltiin kohdekieli. Metakieli tarkoittaa suoraan käännettynä "kielen kieltä". Toisin sanoen metakielen avulla kuvataan konkreettinen kieli, kohdekieli. Metakielenä voidaan käyttää esimerkiksi täsmällistä englantia.

Itse olen käyttänyt jossain määrin Luan ja Pythonin metaominaisuuksia. Tietenkin kaikkien metakielten äiti on vanha kunnon Common Lisp. Ei ole siis tavatonta käyttää kieltä itseään uusien rakenteiden kuvaamiseen. Käytännössä voi olla hyödyllistä tuottaa uusi määritelmä esimerkiksi luokalle. Tämä sinällään voi helpottaa huomattavasti vaikeiden ongelmien ratkaisua. Toisaalta metaohjelmointi ei ole aivan tavallista ja yleisesti tunnettua puuhaa, joten se sinällään voi aiheuttaa päänvaivaa ylläpidolle.

Varsinaisten metaohjelmointiominaisuuksien lisäksi kieliä voidaan käyttää oman määritelmänsä tulkitsemiseen. Toisin sanoen kielellä itsellään voidaan kirjoittaa oman itsensä tulkki, jota metasirkulaariseksikin sanotaan. Toisaalta esimerkiksi C:n tapauksessa kielen kääntäjä voidaan kirjoittaa C:llä.

Viimeinen käsitelty pari oli klassinen arvo vs. olio. Tässä tapauksessa olio ei tarkoita perinteistä olio-ohjelmoinnista tuttua oliota, joka sisältää sekä attribuutteja että metodeja. Tämä olio voidaan ajatella primitiivisimpänä mahdollisena, joka sisältää vain uniikin identiteetin sekä tilan, jota voidaan muuttaa.

On oleellista erottaa arvon käsite edellä määritellystä oliosta. Arvot ovat oleellisesti muuttumattomia. Esimerkiksi luku viisi tai merkkijono "kissa" ovat arvoja. Voidaan ajatella, että kutakin arvoa löytyy ideaalista universumista tai Platonin sanoin ideamaailmasta vain yksi kappale. Muut näkemämme arvon ilmentymät ovat vain näiden ideaalisten arvojen eräänlaisia kopioita tai varjoja. Käytännössä käyttämiämme arvoja voidaan ajatella riittäviksi korvikkeiksi eikä tällä filosofisella jaolla ole sen suurempaa merkitystä. Se kuitenkin tarjoaa mielenkiintoisen viitekehyksen, jonka kautta hahmottaa asiaa laajemmin.

Samaa ajatusta voisi kenties soveltaa myös olioihin. Vai voidaanko? Arvojen ja olioiden olennainen ero syntyy mielestäni siitä, että arvot ovat ajattomia. Ne ovat olemassa ajasta riippumatta. Jokaisen olion tulee syntyä kerran. On kenties absurdia sanoa, että olio on olemassa jo ennen syntymäänsä. Kuitenkin jotta olio voi syntyä, tulee olla olemassa malli, joka esittää sen abstraktin muodon. Ja jossain määrin järkevää voisikin olla ajatella malleista samoin tavoin kuin arvoista.

Sen lisäksi, että jokaisen olion tulee syntyä, voi olla että joidenkin niistä tulee kuolla. Se, tuleeko kaikkien olioiden kuolla joskus onkin jo filosofinen kysymys... Olioiden kuolemaan liittyy mielenkiintoinen ongelma siitä, mitä tämän jälkeen tulisi tehdä. Olio voidaan yksinkertaisimmillaan unohtaa. Toisaalta voisi olla mielekästä, että vanhat oliot tekisivät tilaa uusille. Tämä siis olettaen, että olioiden elintila olisi rajallinen. Tässä tapauksessa tulisi hyödyntää jotain sopivaa mekanismia joiden avulla oliot voisivat viestiä ympäristölleen, että on tullut heidän aikansa poistua. Tai vaihtoehtoisesti ympäristö voi tarkkailla sisältöään ja poistaa aikansa eläneitä olioita.

Tätä kautta päästään kiinni ohjelmoinnissa oleelliseen muistinkäsittelyyn. Esitetyt tavat ovat analogisia manuaalisen ja automaattisen muistinkäsittelyn käsitteiden kanssa. Esimerkiksi C++:ssa oliolle (siis olio-ohjelmoinnin oliolle!) voidaan määritellä destruktori, joka huolehtii sen jälkien siivoamisesta. Jälkimmäinen lähestymistapa on lähellä automaattista roskienkeruuta.

Ohjelmistojen tapauksessa on oleellista muistaa myös, että on olemassa ns. staattisia olioita, jotka elävät ohjelman elinajan aina sen käynnistyksestä sulkemiseen asti. Vielä mielenkiintoisempi tapaus ovat oliot, jotka elävät ohjelman tilasta huolimatta. Toisin sanoen mikäli ohjelma suljetaan ja käynnistetään uudelleen ovat samat vanhat oliot yhä ohjelmassa. Tällöin puhutaan persistenteistä olioista. Olioiden voidaan ajatella olevan horroksessa sen aikaa kun ohjelma on suljettuna.

Persistentit oliot ovat käsitteenä varsin kiehtovia. Niitä voidaan matkia myös kielissä, jotka eivät todellisuudessa toteuta persistenttejä olioita hyödyntämällä sarjallistamista. Tässä tapauksessa oliot kirjoitetaan tietyssä muodossa levylle. Toki tällä lähestymistavalla on ongelmansa. Käsittääkseni suurin ongelma liittyy siihen, että kaikkea ei yksinkertaisesti voida sarjallistaa. Voisi arvella, että sykliset rakenteet voisivat olla vähintään haastavia sarjallistajan kannalta.

Todelliset persistentit oliot voivat olla mielenkiintoisia myös todellisen kehittämisen kannalta. Mitä tapahtuu, jos olion alkuperäisen luokan määritelmä muuttuu? Tätä varten on lienee rakennettava erityinen mekanismi, joka huolehtii olioiden päivittämisestä uutta luokkaa vastaavaksi silti säilyttäen sen kartuttaman tiedon oleellisilta osin.

Vielä palataksemme olioon ja sen tilaan on tärkeää muistaa, että tilan käsitettä voidaan yleistää. Tila voi siis olla tilojen kokoelma tai tilojen kokoelman tilojen kokoelma tai jne. Tätä kautta päästään käsiksi moniulotteisten taulukkojen käsitteeseen. Pienellä nikkaroinnilla ja käsitteen venyttämisellä tilana voidaan ajatella jopa lienee C:stä tuttuja abstrakteja tietotyyppejä, jotka sinällään ovat vain omia pieniä kokoelmiaan.

Luennolla tehtiin mielenkiintoinen havainto liittyen taulukkojen indeksointioperaattoriin. Ensinnäkin indeksin avullahan viitataan tietyssä muistiosoitteessa olevaan tietoon. Se, kuinka notaatiota tulkitaan, riippuu Starcheyn määrittelemistä L-arvon (lausekkeen vasen puoli) ja R-arvon (lausekkeen oikea puoli) käsitteistä. Mikäli viittaus tehdään sijoituslauseen vasemmalla puolella, tarkoittaa se sitä, että kyseisessä indeksissä sijaitsevaan osoitteeseen sijoitetaan. R-arvon tapauksessa kyseisessä indeksissä sijaitseva arvo haetaan ja sijoitetaan lausekkeen vasemmalla puolella olevaan olioon.

Luennon suurin anti liittyi nimenomaan esitettyihin pareihin sekä niiden kautta käytyyn dialogiin. Mielestäni olion määritteleminen uudelleen ja sitä kautta saavutettu abstraktio auttoivat ymmärtämään paremmin sitä, millainen filosofinen perusta moderneilla kielillä on.

tiistai 13. tammikuuta 2009

Luento 2 - Lausekkeista ja vähän muustakin

Toinen luento keskittyi erityisesti lausekkeisiin ja niiden merkitykseen ohjelmointikielissä. Lausekkeita voidaan pitää kaikkien ohjelmointikielien lähtökohtana. Kullakin kielellä on formaali määritelmänsä, lausekkein kuvattu kielioppi, joka määrää kielen syntaksin ja sen tukemat ominaisuudet.

Useat kielet käyttävät samankaltaista, luontevaa syntaksia peruslaskutoimitusten kuvaamiseen. Lisäksi sijoitukseen käytettävä merkki "=" on pääosin vakiintunut, joskin poikkeuksia Pascal esimerkkinä löytyy. Olen samaa mieltä luennoitsijan kanssa siitä, että merkintä ei ole erityisen onnistunut, koska se sotii useimpien henkilöiden matematiikasta peräisin olevaa intuitiota vastaan. Tämän vuoksi Pascalin käyttämä käsittääkseni nimenomaan matematiikasta peräisin oleva ":=" tapa merkitä sijoitusta tuntuu luonnollisemmalta valinnalta.

Toinen seikka, jonka kanssa "=" notaatio saattaa aiheuttaa ongelmia on yhtäsuuruusoperaattori, jonka avulla voidaan testata esimerkiksi ovatko annetut arvot samoja. Koska "=" varaa sijoituksen, yhtäsuuruuden testaamiseen käytetään tavallisesti "==" merkintää.

Valitut tavat merkitä asioita lienevät periytyneet pitkälti vanhemmista kielistä ja siten vakiintuneet "de facto" standardeiksi. Jos kukin kieli toteuttaisi oman tapansa ilmaista esimerkiksi juurikin sijoitusta, tietäisi se enemmän päänvaivaa ohjelmoijille. Toisaalta turhan eksentrinen kieli jääneekin miltei automaattisesti marginaali-ilmiöksi.

Lausekkeiden lisäksi on oleellista muistaa, että kielen kieliopin kuvaamiseen tarvitaan muutakin. Kielioppi voidaan antaa lausekkeiden muodostamana kuvauksena, joka tarvittavissa kohdin sisältää sisäkkäiset lausekkeet sallivan rekursion, eli kutsun lausekkeeseen itseensä, sekä terminaattorit eli tavallisesti esimerkiksi lukuarvot ja muut vastaavat. Tällä abstraktilla kuvauksella pääsee alkuun. Ongelmiin päädytään, kun joudutaan miettimään operaatioiden järjestystä. Esimerkiksi kertolasku on matematiikassa "arvokkaampi" operaatio kuin summaus- tai vähennysoperaatio.

Operaatioiden arvojärjestykseen voidaan ottaa kantaa itse kuvauksessa. Tämä onnistuu määrittelemällä kielioppi oikealla tavoin. Tässä tapauksessa puhutaan konkreetista kieliopista. Perusajatuksena on osittaa abstrakti kuvaus pienempiin alijoukkoihin, joissa kussakin rekursio on rajattu siten, että kaksiosaisen operaation alkuosa voi kutsua alijoukkoa itseään ja loppuosa toista alijoukkoa arvokkaampaa joukkoa. Tällä tavoin operaatioiden välinen arvojärjestys saadaan rakennettua itse kielioppiin. Olennaista kieliopin konkreettisessa esityksessä on myös se, että tällaisen kieliopin avulla voidaan jäsentää, mikä ei abstraktia kielioppia käyttäen ole mahdollista! Abstraktia kielioppia onkin kenties helpompi ajatella apuvälineenä, jonka avulla konkreetti kielioppi voidaan johtaa.

Arvojärjestyksen määrittely ei välttämättä riitä kaikissa tapauksissa. On mahdollista, että kieliopin avulla muodostettu lauseke sisältää vierekkäisiä, samanarvoisia operaattoreita. Tässä tapauksessa puhutaan assosiatiivisuudesta. Ohjelmointikielessä voidaan määritellä, miten kyseinen tapaus käsitellään. Voi olla esimerkiksi mielekästä, että laskennan tarkkuuden kannalta kieli pyrkii arvioimaan lausekkeita siten, että lopputulos on numeerisesti mahdollisimman tarkka. Toisaalta voisi kuvitella, että joissain tapauksissa pienimuotoista laskentaa voisi harrastaa jopa rinnakkain. Tämä kuitenkin menee jo turhan tekniselle tasolle. :)

On huomattava, että perinteinen peruskoulumatematiikasta tuttu notaatio kirjoittaa laskuja ei ole ainoa mielekäs tapa. Vaihtoehtoina esiteltiin puolalainen notaatio ja sen variantit. Perusajatuksena on, että operaattoreita ja niille syötettäviä arvoja arvioidaan hieman eri tavalla. Esimerkiksi operaattorin voidaan ilmaista itse laskun alussa ja käytetyt arvot vasta operaattoreiden jälkeen. Käsittääkseni kyseistä tapaa syöttää tietoa on käytetty ainakin joissain vanhemmissa laskimissa sen nopeuden vuoksi. Ei siis ole välttämättä syytä luottaa tuttuun ja turvalliseen tapaan vaan toisinaan hieman erilainen notaatio voi olla hyödyllinen. Toisaalta vaihtoehtoisen tavan hyödyntäminen voi olla mielekästä jo ihan oppimisen kannalta, onhan yksinkertainen pinoa hyödyntävä postfix (eli numerot ensin ja operaatiot vasta sitten) notaatiota tukeva tulkki helppoa kirjoittaa. Tätä kautta voi sitten siirtyä hieman haastavampiin ongelmiin.

Abstraktiin kielioppiin palatakseni voisin vielä mainita sen, että abstraktin kieliopin avulla koostettu lauseke voidaan esittää puurakenteena. Puurakenteessa mielenkiintoista on sen yksikäsitteisyys, mitä kieliopin peruskuvauksella ei ole. Tämä johtuu olennaisesti tavasta, jolla puita luetaan. Mielenkiintoisena kuriositeettina puumainen esitystapa (tunnetaan myös nimellä AST, Abstract Syntax Tree eli abstrakti syntaksipuu) muistutti minua eräästä vastaavanlaisesta käytännön ongelmasta.

Graafeja (tavallisesti DAG, Directed Acyclic (eli syklitön) Graph) evaluoitaessa voi olla mielekästä optimoida prosessia siten, että operaatioita edustavia noodeja ryhmitellään taustalla (ts. siten ettei käyttäjä näe sitä) siten, että kukin ryhmä sisältää mahdollisimman monta toisiinsa liittyvää operaatiota, kuten abstrakti syntaksipuu ikään. Yhdistämällä useita operaatioita toisiinsa voidaan säästää huomattavasti aikaa, koska muuten atomisia operaatioita voidaan ajatella komposiittina, joka voidaan prosessoida sellaisenaan. Tämä onkin eräs osoitus siitä, että kieliopeista tutuista menetelmistä on hyötyä myös niiden ulkopuolella.

Kenties eräs olennaisimpia asioita, jonka omaksuin, oli matematiikan tukema havainto siitä, että kaikki kielet ovat sukua toisilleen. Ainakin erittäin korkealla, initiaalialgebrojen tasolla. Tarkoittaako tämä myös automaattisesti sitä, että millä tahansa kielellä kirjoitettu ohjelma voidaan kääntää toiselle kielelle? Sinänsä ajatus tuntuu hieman absurdilta ottaen huomioon, että kieliä on niin monenlaisia ja ne voivat erota erittäin paljon toisistaan. Kuitenkin ottaen huomioon matemaattisen taustan uskon, että tämä on mahdollista olettaen, että aikaa on käytettävissä rajaton määrä.

Luennon olennaisin anti liittyi nimenomaan kielen kuvaamiseen alkeellisimmalla, eli kieliopin tasolla. Todellisuudessa kieliopit eivät välttämättä ole kovinkaan monimutkaisia. Hyvä esimerkki tästä on Luan syntaksi. Esimerkiksi Pythonin syntaksi on jo aavistuksen monimutkaisempi mutta edelleen varsin luettava. Käytännössä suurin haaste olisikin oman kielen ja syntaksin laatiminen. :)

maanantai 12. tammikuuta 2009

Luento 1 - kielien moninaisuudesta ja luokittelusta

Ohjelmointikielten periaatteet, Jyväskylän yliopiston kurssikoodiltaan TIES542, kurssin suositeltavin suoritustapa on oppimispäiväkirjan kirjoittaminen, jota tässä paraikaa kirjoitan. Tämä sinällään on virkistävä, ainutlaatuinen kokemus ja toivon kehittyväni "päiväkirjailijana" kurssin kuluessa.

Taustoistani voisin mainita sen verran, että olen ohjelmoinut kaikenlaista pientä aina ala-asteikäisestä lähtien. Varsinainen "valaistuminen" ohjelmoinnin suhteen tapahtui tosin vasta yliopisto-opintojeni alkuvaiheessa käytyäni Ohjelmointi 2 sekä GKO-kurssit. Voisi sanoa, että tämän jälkeen maailma ei ole ollut entisensä.

Sittemmin useat eri kielet, mukaan lukien Perl, PHP, Python ja Lua, ovat tulleet enemmän tai vähemmän tutuiksi. Erityisesti olen tykästynyt Pythoniin sen joustavuuden ja mukavan syntaksin vuoksi. Funktiokielet ovat toistaiseksi jääneet vähemmälle huomiolle. Osallistun kurssille avoimin mielin ja toivon kurssin jälkeen hahmottavani ohjelmointikieliä hieman laajemmasta näkökulmasta.

Luennolla listattiin yleisön tuntemia ohjelmointikieliä. Suuren osan tunsin ainakin nimeltä. Kenties yllättävimmät listatut kielet olivat laitteenohjaukseen käytettävä LabView sekä COMMAND.COM, jota en ollut aiemmin hahmottanut ohjelmointikielenä. LabView herätti kiinnostukseni erityisesti, koska mainittiin, että kyseessä on graafinen ohjelmointityökalu. Wikipedian sivulta varmistin, että kyseessä on tietovuoarkkitehtuuriin pohjautuva kieli.

Tietovuoarkkitehtuuriin pohjautuvat järjestelmät ovat sydäntäni lähellä. Olenhan jo useita vuosia työskennellyt niiden parissa erityisesti Blenderiin liittyen. Useat grafiikan ja äänen käsittelyyn ohjelmistot hyödyntävät kyseistä arkkitehtuuria. Paraikaa kehitän/suunnittelen omaa pientä ohjelmistoani. Mielestäni tietovuolähestymistapa tarjoaa joissain tapauksissa oivan vaihtoehdon perinteisille, tekstipohjaisille kielille. Linux.com listaakin eräitä graafisia "lasten" ohjelmointikieliä ("lasten" koska mikään ei kiellä ainakaan testaamasta noita).

Veikkaisin, että graafisten kielten rajat tulevat vastaan kompleksisuuden hallinnassa. Tämä sinällään on jokaisen ohjelmoijan perustehtävä. Miksi muuten ohjelmointikieliä olisi ylipäätään kehitetty? Nähdäkseni kukin kieli pyrkii luomaan tehtäväkenttään mahdollisimman hyvin sopivan abstraktion. Toki kielen luonnissa otetaan muitakin aspekteja huomioon. Kuitenkin kullakin kielellä nähdäkseni on jokin perustehtävä, jonka se pyrkii täyttämään mahdollisimman hyvin.

Kielten abstraktiuteen liittyen kannattaa muistaa myös kielen taso suhteessa laitteistoon. Esimerkiksi C ja C++ likimain pakottavat kehittäjän ottamaan kantaa muistinhallintaan. Tämä voi olla hyvä asia, mikäli muistia ja sen käyttöä halutaan hallita tarkasti. Toisaalta esimerkiksi Java ja mielestäni vielä abstraktimpi, jopa pseudokoodimainen Python, huolehtivat muistista kehittäjän puolesta. Pythonin heikkoutena voidaan pitää sen hitautta, mutta toisaalta nopeutta vaativia ohjelmiston osia voidaan toteuttaa C:tä käyttäen. Ei ole siis välttämättä järkevää rajoittua ainoastaan yhden kielen käyttöön.

On huomattava, että myös perinteiselle ohjelmointikielelle voidaan antaa graafinen esitys. Ohjelmistojen visualisointiin perehtyvä tutkimus pyrkii nimenomaan tähän. Uskon, että tekstimuotoisen esityksen lisäksi graafiset tavat esittää asioita helpottavat kehitystyötä. Parhaimmillaan järjestelmästä on tarjolla monen eri tason näkymiä. Tämä tosin menee jo ohjelmistoarkkitehtuurien puolelle. :)

Se, kuinka ohjelmointikieli voidaan määritellä, on oleellinen kysymys. Eräs perinteinen määritelmä tälle on se, että kieli on ohjelmointikieli, jos sillä itsellään voidaan kirjoittaa kääntäjä/tulkki kielelle itselleen. Esimerkiksi PyPy toteuttaa Pythonin kielellä itsellään. Määritelmänä tämä ei kuitenkaan ole täysin kattava. Esimerkiksi tätä kriteeriä käyttäen suomenkieli on ohjelmointikieli, koska sen kielioppi voidaan määritellä kielellä itsellään, vai onko? Mikä onkaan perinteisen kielen (suomi, englanti, ym.) ja ohjelmointikielen välinen ero?

Perinteisillä kielillä voi ohjelmoida. Ei tosin tietokoneita. Vaan esimerkiksi tämän tekstin lukijaa. Se mitä kirjoitan saattaa vaikuttaa tai olla vaikuttamatta lukijaan. Ohjelmoinnissa on siis oleellisesti kyse vaikuttamisesta. Kuitenkin koska kyseessä on tietotekniikan kurssi rajaan ohjelmoinnin tarkoittamaan tässä yhteydessä nimenomaan tietokoneiden ohjelmointia vaikka käsitettä sinänsä voisi laajentaa vaikka kuinka.

Perinteisten kielten ja ohjelmointikielten väliseen eroon päästään käsiksi ottamalla huomioon konteksti, jossa niitä käytetään. Perinteistä kieltä käyttäen ei voida ohjelmoida tietokoneohjelmia, ainakaan kovin menestyksekkäästi.

Perinteisten kielten kielioppi voidaan tavallisesti määritellä formaalisti, kuitenkin ottaen mahdolliset poikkeukset huomioon! Tästä syntyykin perinteisten ja ohjelmointikielten merkittävin ero. Ohjelmointikielet ovat täysin formaaleja. Niiden täytyy olla, jotta niitä voidaan käyttää tietokoneen kannalta yksiselitteiseen kommunikointiin.

Mikäli palaa aiemmin esitettyyn ohjelmointikielen alkeelliseen määritelmään (ohjelmointikielellä voidaan ohjelmoida kielen itsensä kääntäjä/tulkki) voidaan havaita, että määritelmää voisi laajentaa myös siten, että kielellä tulisi voida kirjoittaa kääntäjiä/tulkkeja muille kielille. Toisaalta voidaan kirjoittaa ohjelmia, jotka kirjoittavat koodia joko samalla tai toisella kielellä. Tätä tapaa ohjelmoida sanotaan generatiiviseksi. Kyseinen ohjelmointitapa voi olla parhaimmillaan varsin tehokas. Esimerkiksi ohjelmiston tarjoama ohjelmointirajapinta voidaan generoida automaattisesti halutulle kielelle. Eräs mielenkiintoinen sovellus generatiiviselle ohjelmoinnille on tietääkseni erityisten DSL-kielten toteuttaminen.

DSL (Domain Specific Language) pyrkii kuvaamaan kielen ongelmakentän sanastoa käyttäen. Tämä sinällään antaa ylimääräisen abstraktiokerroksen, joka saattaa helpottaa kehitystyötä. On kuitenkin tärkeää muistaa, että mikään abstraktio ei ole täydellinen. Abstraktio on aina enemmän tai vähemmän tarkka approksimaatio kohteestaan. Se voi näyttää erilaiselta tutkailukannasta riippuen. Joten pahimmillaan DSL voi jopa vaikeuttaa asioita, mutta optimistina uskon, että niistä on enemmän hyötyä kuin haittaa, kunhan tietää mitä tekee. :)

Jos kaikista maailman ohjelmointikielistä haluaa löytää edellä mainittujen seikkojen lisäksi yhteisiä piirteitä, käytetään niissä kaikissa symboleja. Symboli on olennainen osa ohjelmointikieltä tai kieltä yleisemminkin, jota ilman ei yksinkertaisesti voi kommunikoida. Toki symbolin puute sinänsä voi olla jo symboli (hyvä esimerkki tästä on välilyönti).

Ohjelmointikielten historia ja niiden kehittyminen aikojen kuluessa kuvastavat hyvin kunkin ajan henkeä. Aluksi ohjelmointikielet olivat, luonnollisesti, hyvin lähellä ns. rautatasoa. Kielet kehittyivät, kuten aiemmin jo viittasinkin kielten käyttötarkoitukseen, tiettyä käyttötarkoitusta varten. Kenties hieman yllättäen useat vanhoina pidetyt kielet ovat yhä hengissä. Kielen ikä ei siis välttämättä tarkoita sitä, että kieli olisi jotenkin huono. Joku voisi jopa väittää, että kieli paranee vanhetessaan, kuten viini ikään. Mielestäni kielen menestymisen kannalta on kuitenkin oleellista, että kieltä kehitetään aktiivisesti eikä sen anneta rämehtyä. Sama pätee myös sovelluksiin.

Se, miten kieltä tulisi kehittää, on hyvä kysymys. Mielestäni on olennaista, että kieltä ei päästetä paisumaan liiaksi vaan se pidetään lähellä varsinaista käyttötarkoitustaan. Esimerkiksi Lua on jossain määrin menestyksekkäästi onnistunut tässä. Pythonin tapauksessa uusimman version 3.0:n tapauksessa kieltä siivottiin ja taaksepäinyhteensopivuus rikottiin. Toisaalta uuteen versioon siirtymistä helpotettiin huomattavasti antamalla kehittäjien käyttöön työkalut sitä varten. Toisaalta myös vanhaa 2.x -sarjaa tuetaan yhä.

Tämän hetken trendeistä vahvin on lienee rinnakkaisuus ja sen tuomat vaatimukset ohjelmoijille sekä ohjelmointikielille. Koska en ole juurikaan perehtynyt rinnakkaisohjelmointiin, olen hieman jäävi kommentoimaan asiaa. Kuitenkin uskoisin, että ajan kuluessa saatavilla olevat työkalut paranevat. Toisaalta myös käsittääkseni luonnostaan hyvin rinnakkaisuutta tukeva ohjelmointiparadigma, funktio-ohjelmointi voi saavuttaa lisää suosiota.

Toisaalta uskon, että useita ohjelmointiparadigmoja tukevat kielet tulevat yleistymään tai ainakin lisäämään suosiotaan. Ohjelmointiparadigman käsite sinänsä on hieman ongelmallinen. Kun esimerkiksi sanotaan, että jokin kieli on oliokieli, mitä se todellisuudessa tarkoittaakaan. :) Kielten, ja erityisesti moniparadigmakielten, jaottelu on vähintäänkin haastavaa.

Steve McConnell puhuu kielessä ohjelmoinnin ja kieleen ohjelmoinnin käsitteistä. Hänen olennaisin sanomansa tässä yhteydessä on nähdäkseni se, että ohjelmoijan ei tulisi rajoittua käyttämään ohjelmointikielen tarjoamia keinoja vaan pyrkiä pikemminkin soveltavaan ratkaisuja, jotka tulevat kielen ulkopuolelta ja ovat tarpeen mukaisia. Tämä ajatus osaltaan auttaa ymmärtämään suhtautumistani ohjelmointiparadigmoihin sekä -kieliin. Osa ohjelmoijan ammattitaitoa on oikean työkalun ja tekniikan hyödyntäminen oikeassa paikassa olemassa olevien rajoitteiden (esim. työpaikan käyttämät teknologiat) puitteissa.

Yhteenvetona totean, että mielestäni tämän "päiväkirjoittelun" myötä pääsin jossain määrin vauhtiin. Voi olla, että myöhemmissä pohdiskeluissa palaan vielä ohjelmointikielen määritelmän pariin. Aiheessa riittää kaluamista vielä.