keskiviikko 18. maaliskuuta 2009

Yhteenveto

Kävin blogin postaukset läpi aina 15. luentoon asti ja kommentoin niitä aina tarpeen mukaan. Tuota uudempiin oli hankalampaa keksiä kommentoitavaa. Joissain tapauksissa opittu asia oli tuonut hieman toisenlaista näkökulmaa kirjoituksiin, joten tässä mielessä kommentoinnista oli mielestäni hyötyä.

Kurssin tavoitteiksi on määritelty seuraavat:
  1. Nostaa opiskelijoiden ohjelmointikielikäsityksen abstraktiotasoa.
  2. Valmentaa opiskelijat arvioimaan ohjelmointikieliä eri kriteereillä
  3. Antaa opiskelijoille riittävät teoreettiset työkalut ohjelmointikielten tutkimuksen seuraamiseen.
Mielestäni olen ainakin jossain määrin pystynyt saavuttamaan näitä tavoitteita. Kurssi nitoi mukavasti yhteen muutaman viime vuoden aikana oppimaani. Toisaalta se tarjosi myös uutta suuntaa opittavien asioiden suhteen. Lisäksi sain virikkeitä omiin projekteihini.

On selvää, että jossain vaiheessa (ehkä viikonloppuprojektina?) voisi vilkaista ainakin Haskelia ja LISPiä. Funktio- ja logiikkaohjelmointitaidot saattaisivat olla hyödyllisiä jo ainakin sen vuoksi, että oppii näkemään nykyistä osaamistaan hieman eri valossa. Logiikkaohjelmointia olisi hyvä opetella jo sen vuoksi, että pääsisi kiinni paremmin tekoälyasioihin.

Kurssin suurin anti oli kenties se, että nyt käyttämiään ohjelmointikieliä osaa tarkastella hieman toisin. Tarvittaessa osaan toteuttaa kieliin tarvitsemiani konstruktioita (esim. piirteet!). Jossain määrin kielen suunnittelu kytkeytyy laajemmin ohjelmistosuunnitteluun. Jos kieli laajennettuna on tarpeeksi ilmaisuvoimainen, auttaa se myös ohjelmistojen toteuttamisessa. Uskoakseni ilmaisuvoimaisuudesta seuraa tiettyä selkeyttä, joka edesauttaa koodin tarkoituksen kommunikointia. Tähän juuri DSL:t (Domain Specific Language) juuri pyrkivätkin.

Oppimispäiväkirja osoittautui oppimismuotona varsin päteväksi. Mielestäni ei haittaisi vaikka useammatkin kurssit hyödyntäisivät sitä. Ennemmin kirjoitan ja pohdin kuin pänttään tenttiin. Etuna on myös se, että samalla tulee dokumentoitua ja pohdittua oppimaansa. Lisäksi joskus myöhemmin voi sitten naureskella kirjoittelulleen, kun tietää asiat paremmin.

tiistai 17. maaliskuuta 2009

Demo 9 - kommentit

Yhdeksännen ja viimeisen demokerran tehtävät löytyvät osoitteesta http://users.jyu.fi/~antkaij/opetus/okp/2009/demot/9.html. Tällä kertaa demoihin osallistui lisäkseni yksi opiskelija, ohjaaja sekä luennoitsija. Tehtäviä teimme kumulatiivisesti laskien kolme kappaletta, joista itse tein yhden. Demokerta kesti 15 minuuttia, mikä oli ainakin osaltani jonkin sortin ennätys.

Tehtävät eivät olleet vaikeita, joten vähäinen osallistujien määrä oli jossain määrin yllätyksellistä. Ensimmäisessä tehtävässä (tein tämän) käsiteltiin Links-kieltä, joka on suunniteltu erityisesti www-käyttöä varten. Ajatuksen tasolla Links on loistava.

Käytännössä www-kehitys on edelleen useimmiten sitä, että kehittäjä joutuu kirjoittamaan koodia tavallisesti ainakin kolmella eri kielellä (PHP/Python/Ruby/..., JavaScript sekä (X)HTML tms. + CSS (tyyli)). Links pureutuu nimenomaan tähän ongelmaan ja pyrkii luomaan ratkaisun, joka nitoo nämä erilliset osat yhteen. Mielestäni Links onnistuukin tässä jossain määrin. Kuitenkin HTML-pohjainen koodi näkyy pahasti läpi.

Uskonkin, että tämä osa tulisi abstrahoida niin pitkälle kuin mahdollista. Kehittäjän ei tulisi välittää HTML-koodista vaan pikemminkin abstraktista rakenteesta, joka asemoidaan kohdalleen (käyttöliittymän suunnittelijan tehtävä). Tässä tapauksessa sovelluskehittäjän pääasiallinen tehtävä olisi logiikan suunnittelu. Mielestäni esim. Weblocks ja Seaside onnistuvat tässä suhteessa paremmin.

Oma www-kokemukseni rajoittuu lähinnä PHP:lla pelailuun sekä Djangoon, jota käytin ollessani mukana sovellusprojekti-kurssilla. Django osoittautui yllättävän hyväksi. Tosin erityisesti AJAX-toiminnallisuuden toteuttaminen jätti tuolloin vielä toivomisen varaa. AJAX ikäänkuin liimattiin kokonaisuuden päälle, mikä ei ollut varsin mukavaa. Myös HTML näkyi hieman turhan paljon läpi. Onneksi Djangon template-pohjainen ratkaisu auttoi tässä paljon. template-perinnän avulla suurin osa pakollisesta rojusta (headerit, navigaatio ym.) jäi hierarkian vanhimpiin luokkiin.

maanantai 16. maaliskuuta 2009

Luento 18 - Oliokielten erityiskysymyksiä II

Tällä kertaa pidettiin kurssin viimeinen luento. Luennolla palattiin oliokielten pariin. Erityisesti moniperintä sai huomiota. Lisäksi luennon lopuksi kurssilla käsitellyt asiat koottiin yhteen.

Moniperintä on varsin hyödyllinen ominaisuus. Mielestäni moniperinnän suurin hyöty saadaan käytettäessä mixinejä. Luennolla esiteltiinkin C++ toteutus mixinien ja malline (template) -luokkien käytöstä.

Moniperinnän ongelmallisuus tulee ilmi, kun perintähierarkia sisältää timantin eli ts. jos A on ylin luokka, B ja C perii A:n, D perii B:n ja C:n. Tässä tapauksessa ei ole selvää kuinka D tulisi koostaa, koska se perii A:n kahta kautta. Mielestäni tässä tapauksessa on hyödyllistä sisällyttää ohjelmointikieleen päättelylogiikka, jonka perusteella asia ratkaistaan. Voidaan esimerkiksi päättää, että viimeksi perityt ominaisuudet ovat vahvimpia, jolloin ne jäävät voimaan.

Luennolla esitellyssä esimerkissä moniperinnän avulla syöte/tulostevirran käsittelijää laajennettiin synkronointia varten lisätyllä lukolla. Tässä tapauksessa moniperintää olisi voinut kiertää komposition avulla siten, että lukko olisi sisällytetty synkronoituun luokkaan. Toisaalta tämä ratkaisu menee nopeasti sotkuiseksi, koska luokkien synkronoidut toteutukset joutuvat hyödyntämään esim. template method -mallia seuraavasti:


...
def read(self):
return self.lock.synchronize(super(SynchronizedReadStream, self).read)
...


Toteutusta saa siistimmäksi dekoraattorin avulla:


...
@synchronized
def read(self):
return super(SynchronizedReadStream, self).read()
...


Dekoraattorin toteutus huolehtii lukon asettamisesta ja vapauttamisesta. Toisaalta dekoroinnin voisi tehdä myös metodin kutsuvaiheessa.

Ducasse ym. ovat kehittäneet ongelmaan hieman erilaisen ratkaisun, piirteet (traits). Pohjimmiltaan kyse on koostamisesta. Piirteet kuitenkin onnistuvat kiertämään moniperinnän ongelmia siten, että luokan käyttämien piirteiden sisältämän koodin voidaan ajatella kopioituvan luokkaan itseensä dynaamisesti.

Luennolla mainittiin myös binäärimetodeihin liittyvästä ongelmallisuudesta. Binäärimetodissa ajatuksena on, että metodille annettujen parametrien (binääri -> 2 kpl parametreja!) luokat vaikuttavat tulokseen. Luokat siis vaikuttavat suoraan tulkintatapaan. Ongelmaa voi kiertää joko multimetodien tai This-avainsanan (tyyppitason vastine this-avainsanalle) avulla.

Multimetodien käyttö muistuttaa esim. konstruktorien ylikirjoitusta(?) (overloading), jossa käytetty konstruktori määräytyy kutsun perusteella. Tässä tapauksessa kutsun luokat pitää kuitenkin ottaa dynaamisesti huomioon toisin kuin edellä!

Multimetodien toteutusta voidaan ajatella sääntöpohjaisesti (tyypit sidotaan säännön avulla haluttuun metodiin) tai dekoraattorien avulla. Näistä dekoraattoritoteutus tuntuu varsin näppärältä.

This-avainsana vaikuttaa ideana varsin elegantilta. Käytännössä sen käyttö mahdollistaa sen, että tyyppi propagoituu luokan aliluokkiin automaattisesti eikä erillisiä tyyppitarkistuksia tarvita kuten ennen. Luennolla mainittiin, että tämä tapa toimisi vain staattisesti tyypitetyissä järjestelmissä. Aivan tarkkaa syytä siihen, miksi näin on, en saanut selville.

Intuitiivisesti tuntuu, että This-avainsanaa voisi matkia dekoraattorin avulla. This-avainsanan voisi mallintaa näennäisluokan (dummy) avulla. Tämän voisi sitten välittää dekoraattorille (@method_types(This) (voisi olla geneerinen jolloin voisi mallintaa myös oikeita tyyppejä (Int, Str jne.))), joka osaisi tehdä tulkinnan (This viittaa dekoroidun metodin luokkaan!).

En ole varma olisiko This-avainsanasta dynaamisesti tyypitetyssä kielessä mitään käytännön hyötyä. Ainakin tyypit voisivat olla vapaaehtoisina päteviä. Olen joskus toteuttanutkin omia tyyppejä (esim. Color ja ConstrainedValue). Omien tyyppien toteuttaminen on arkkitehtuurin selkeyden kannalta varsin hyvä asia. Esim. ConstrainedValuen tapauksessa pystyin kapseloimaan rajoituspiirteen tyyppiin itseensä. Tämä tarkoittaa sitä, että muun koodin ei tarvitse välittää rajoituksien tarkastamisesta, koska tyyppi itse osaa huolehtia siitä.

Luennon lopun yhteenvedossa lähinnä kerrattiin kurssin tavoitteet ja oleellisimmat läpi käydyt asiat. Käyn kaikki postaukseni vielä erikseen läpi ja kirjoitan kommentoin niitä. En aio ruveta sensuroimaan itseäni vaan pidän kommentit erillisinä. Samalla tulee nähtyä kuinka asioita on tullut opittua. Lisäksi kirjoitan erillisen postauksen, jossa tarkastelen oppimaani suhteessa tavoitteisiin ja käyn läpi oleellisimpia kommentoidessani tekemiäni huomioita.

keskiviikko 11. maaliskuuta 2009

Demo 8 - kommentit

Kahdeksannen demokerran tehtävät löytyvät täältä. Tehtävät olivat aikaisempaa helpompia, mutta silti sopivan työläitä. Erityisesti viimeiset bonustehtävät (2-6) veivät aikaa. Tällä kertaa panostin demoihin tavallista enemmän tehden perustehtävien lisäksi juurikin mainitut tehtävät.

Perustehtävistä antoisin oli kenties tehtävä 4, jossa pyydettiin katsomaan video ja vastaamaan annettuihin kysymyksiin. Videossa oli pohjimmiltaan kyse kielen kasvattamisesta. Oleellista onkin kuinka kielen tulisi voida kasvaa. Aina ei ole selvää, että kieltä voidaan laajentaa. Kuitenkin käyttäjän kannalta laajennusmahdollisuudet ovat suotavia.

Lisäksi kielen tulee olla kasvatettavuuden lisäksi tarpeeksi ilmaisuvoimainen. Mielestäni on myös oleellista, että oikeaa kieltä käytetään oikeassa paikassa. On jossain määrin mahdotonta suunnitella kieltä, joka olisi parhaimmillaan kaikissa eri tarkoituksissa sellaisenaan. Kielen kasvattaminen auttaa tässä jossain määrin. Kuitenkin on kenties kannattavampaa käyttää käyttötarkoitusta varten jo lähtökohtaisesti suunniteltua kieltä.

Yllä mainittujen seikkojen lisäksi mielestäni on oleellista pitää mielessä, että aina ei ole mielekästä rajoittua vain yhden kielen käyttämiseen. Esimerkiksi Pythonia voidaan pitää ns. liimakielenä. Tämä tarkoittaa sitä, että sen avulla voidaan paketoida suorituskyvyltään nopeita C-moduuleja kokonaisuudeksi. Tässä tapauksessa C:n avulla huolehditaan suorituskykyvaatimuksesta ja Pythonin avulla toteutetaan muu toiminnallisuus. Onhan sitä useimmiten nopeampaa ja mukavampaa kehittää.

Bonustehtävänä sain suunnitella aivan oman, pienen oliokielen. Pyrin pienuuteen yhdistämällä aliohjelmien, metodien, luokkien ym. käsitteen yhteen. Tästä seurauksena loin entiteetin-käsitteen. Lisäksi kieli sisältää staattiset tyypit, jotka tosin varmaan jossain määrin heitän menemään, mikäli jonain päivänä innostun kehittämään kieltä edelleen. Tarkempi kuvaus, esimerkkejä sekä alkeellinen toteutus löytyvät täältä. Testit on toteutettu py.test -työkalua käyttäen. Hyvä kuvaus työkalusta löytyy osoitteesta http://codespeak.net/svn/py/extradoc/talk/pycon-uk-2008/pytest.pdf.

Tiedän ettei kieli ole missään mielessä täydellinen. Erityisesti this-semantiikka kaipaisi virkistystä. Mikäli lähtisin kehittämään kieltä edelleen suuntaisin sen Pythonin laajennuskieleksi. Tässä tapauksessa kielestä voisi käyttää suoraan Pythonin moduuleita, mikä olisi varsin mukavaa.

Luento 17 - Viestinvälityssamanaikaisuus

Viestinvälityssamanaikaisuudessa on kysymys samanaikaisuuden toisesta osajoukosta. Edellisessä postauksessa käsittelin tämän asian rinnakkaista käsitettä, yhteismuistisamanaikaisuutta.

Tätä blogia voidaan pitää viestinvälityksen mediumina. Minä kirjoittajana olen viestin lähettäjä. Lukijoita voidaan pitää vastaanottajina. Toisaalta kommentin jättävä lukija on puolestaan lähettäjä ja minä sekä muut lukijat vastaanottajia. Viestinvälityksessä on siis kolme oleellista komponenttia: lähettäjä, medium (median yksikkö) sekä vastaanottaja.

Viestinvälitystä voidaan varioida monin eri tavoin. Tämä näkyy muun muassa käytetyistä standardeista (TCP/IP, UDP/IP ym.), jotka kukin tarjoavat oman tapansa toimittaa viestejä perille. Riippuen käytetystä menetelmästä voidaan esimerkiksi taata, että viestit saapuvat perille lähetysjärjestyksessä. Menetelmä voi ottaa kantaa myös viestinvälityksen luotettavuuteen.

Mielenkiintoinen variantti on tapaus, jossa lähettäjä ei voi lähettää viestiä ennen kuin vastaanottaja on valmiina vastaanottamaan sen. Tällöin puhutaan kohtaamisesta tai tunnetummin ns. rendezvous-menetelmästä.

Olin kuullut kohtaamisesta aiemmin Adan yhteydessä. Luento kuitenkin selkeytti käsitettä hieman. Kenties hieman yllätyksellisesti kohtaamista voidaan pitää muiden menetelmien prototyyppinä, jonka avulla haluttua menetelmää voidaan matkia. Tuntuukin luontevalta, että klassinen tuottaja-kuluttaja -ongelma ratkeaisi sitä käyttäen mallikkaasti.

Kohtaamiseen pohjautuen on mallinnettu erityisiä viestikieliä. Näistä luennolla esiteltiin Hoaren CSP (Communicating Sequential Processes) sekä piilaskento. CSP:tä voidaan pitää vähemmän matemaattisesti orientoituneen kannalta helpompana.

CSP on imperatiivisen kieleen lisättävä laajennus, joka mahdollistaa erityisten kanavien (vrt. medium) käyttämisen. Oleellisesti kanavien kautta voidaan huolehtia viestinnästä edellä esitellyn kohtaamisen tavoin.

CSP vaikuttaa varsin mielenkiintoiselta, koska se näyttää tuovan luontevan ratkaisun moniin samanaikaisuuden ongelmiin. Käytännössä en ole joutunut kiinnittämään huomiota samanaikaisuuteen. Kuitenkin on selvää, että laskentatehon, erityisesti rinnakkaisen, lisääntyessä niiltä, jotka rohkenevat kutsua itseään ohjelmoijiksi, vaaditaan myös samanaikaisuuden hallinnan taitoja.

Löysin sattumalta Hoaren Communicating Sequential Processes -kirjan. Samalla löytyi myös toinen varsin mielenkiintoinen paikka. En ole mikään CSP-guru, mutta näin nopealla tutustumisella asiaan se vaikuttaa opettelemisen arvoiselta tekniikalta. Sattumalta löysin myös Pythonille toteutetun CSP-laajennoksen. Se näkyy olevan saataville myös useille muille kielille.

Tässä yhteydessä en osaa sanoa sen syvällisempää CSP:stä. Kuitenkin luento osoitti minut tässä suhteessa oikeaan suuntaan. Luennon toista pääasiaa, piilaskentoa tuskin tulen käyttämään yhtä paljon. Konstruktiona piilaskento on kuitenkin aika jännä.

Piilaskento, kuten CSP:kin, lähtee liikkeelle kanavan käsitteestä. Itse asiassa kaikki ajatellaan piilaskennossa kanavan käsitteen kautta. Se pyrkiikin olemaan samanaikaisuudelle sama asia, kuin lambdalaskento on ei-samanaikaisille (sekventiaalisille?) kielille.

Luennon suurin anti oli kenties sen antama näköala eri mahdollisuuksiin huolehtia viestinvälityksestä. Sinällään aihe on kannaltani varsin oleellinen. Olen joutunut miettimään viestinvälitystä useaan otteeseen omassa pienessä käyttöliittymäkirjastoprojektissani. CSP:n kaltainen ratkaisu voisi olla kenties tässä suhteessa potentiaalinen. Pitääkin selvittää jossain vaiheessa, miten samanaikaisuutta voi/kannattaa/pitää ottaa huomioon tämän tyylisessä projektissa.

Samanaikaisuuteen tulen törmäämään myös kuvankäsittelyssä. Kuvankäsittelyn tapauksessa tietyntyyppisiä operaatioita voidaan suorittaa hyvin tehokkaasti rinnakkain. Toisaalta tietyntyyppiset operaatiot (esim. kerneliin pohjautuvat operaatiot, kuten sumennus) ovat ongelmallisia ja tarvitsevatkin hieman toisenlaista tapaa ajatella. Onkin kyseenalaista kannattaako näitä operaatioita suorittaa rinnakkain ja jos kannattaa niin miten se on edullisinta.

Luento 16 - Yhteismuistisamanaikaisuus

Luennon aluksi eroteltiin samanaikaisuuden (concurrent) ja rinnakkaisuuden (parallel) käsitteet. Rinnakkaisesti etenevät tapahtumat ovat toisistaan riippumattomia. Samanaikaisuuden tapauksessa näin ei ole.

Tätä eroa voi kenties havainnollistaa pienellä esimerkillä. Olkoon Aaro ja Eero, jotka rakentavat kumpikin omaa majaansa. Kuopan kaivaminen on rinnakkaista, jos kummallakin on omat työvälineensä ja he rakentavat majaansa omassa paikassaan. Mikäli he joutuvat jakamaan työvälineitään edes jossain määrin, muuttuu tapahtuma käsittääkseni samanaikaiseksi juuri tämän riippuvuuden vuoksi. Voi olla, että Eero joutuu lainaamaan Aaron kirvestä, joka on hänellä käytössä. Tässä tapauksessa lienee luontevaa odottaa, ellei jotain muuta järkevää ole tehtävissä.

Olen taipuvainen näkemään rinnakkaisuuden käsitteen alisteisena samanaikaisuuden käsitteelle. Tuntuu järkevältä, että samanaikaisessa tapahtumassa osia voidaan suorittaa rinnakkain. Samanaikaisuus asettaa tosin yllä esitetyn esimerkin tapaisia rajoitteita, joita puhtaassa rinnakkaisuudessa ei ole.

Luennolla samanaikaisuus jaettiin kahteen ohjelmointikielten näkökulmasta kahteen lohkoon: yhteismuisti- ja viestinvälityssamanaikaisuuteen. Näistä ensimmäinen oli tämän luennon aiheena. Jälkimmäistä käsiteltiin seuraavalla luennolla. On huomattava, että molemmat tavat ovat yleisesti käytössä.

Mistä yhteismuistisamanaikaisuudessa on kysymys? Tässä tapauksessa samanaikaisesti suoritettavat tapahtumat jakavat osan yhteistä muistia, johon kumpikin pääsee käsiksi. Esimerkiksi yllä yhteismuistiin voitaisiin tallentaa Aaron ja Eeron käytettävissä olevat työkalut ja niiden tila (onko käytössä, kuka käyttää).

Yhteismuistin tapauksessa ongelmaksi muodostuu, kuinka hallita tätä yhteistä muistia. Toisaalta kunkin samanaikaisesti suoritettavan tapahtuman tulisi olla synkronoitu keskenään siten, että ne eivät voi olla keskenään virheellisessä tilassa. Toisin sanoen, jos Aarolla ja Eerolla on käytössään vain yksi kirves, ei heidän kummankin tulisi olla mahdollista käyttää samaa kirvestä eri paikoissa samanaikaisesti.

Eräs varsin käytetty ratkaisu on lukkojen käyttö. Tässä tapauksessa haluttu resurssi lukitaan siten, että sitä voi käyttää vain yksi käyttäjä kerrallaan. Toisaalta lukkojen suhteen lienee mahdollista tehdä eri kokoisia rajauksia. Voisikin kuvitella, että ääritapauksessa koko yhteismuistialueen voisi lukita. Tässä mielessä lukot muistuttavatkin tietokantoja ja niiden synkronointia. Pohjimmiltaan kyse onkin samasta ongelmasta.

Mitä yllä mainittu tarkoittaa ohjelmointikielten kannalta? Ohjelmointikieli voi ottaa selvästi kantaa samanaikaisuuteen tai se voi jättää samanaikaisuuden hallinnan kirjastojen vastuulle. Näkisin kirjastototeutuksen (esim. C ja POSIX) joustavampana. Toisaalta luennolla mainittiin, että kielissä, joissa samanaikaisuus on jätetty vähemmälle huomiolle, ei kirjastojen toteuttaminen ole välttämättä aivan yksioikoista. Kielen tulisikin olla määritelty tietyllä tavalla, jotta kirjastoja voisi toteuttaa täsmällisellä tavalla.

Luennolla mainittiin myös kriittisten lohkojen käsitteestä. Kriittiset lohkot suoritetaan siten, että kaikki muut tehtävät on pysäytetty. Lisäksi niiden sisällä olevaa toiminnallisuutta on rajoitettu. Tästä esitettiin myös kehittyneempi ehdollinen versio.

Aaron ja Eeron tapauksessa tämä lienee tarkoittaisi siteä, että lohkon avulla voisi samanaikaisuutta rajoittaa tapahtumien tasolla. Voisi esimerkiksi asettaa rajoitteen, jonka mukaan vain yksi henkilö kerrallaan saa maalata seiniä. Toisaalta tämänkin tulisi olla mahdollista vain, jos maalia on jäljellä ja ulkona on vielä valoisaa.

Luennon loppupuolella esitettiin abstraktimpi ratkaisu, monitori. Monitori kapseloi jaetut muuttujat operaatioiden avulla. Toisaalta vain yksi tehtävä voi käyttää operaatioita kerrallaan. Muut tehtävät asetetaan jonotuslistaan, kuten pankissa ikään. Tämän lisäksi monitori sisältää ehdollisia jonoja. Ehdollinen jono voitaisiin kenties toteuttaa siten, että ehdollisessa jonossa oleva tehtävä siirretään oikean jonotuslistan eteen ehdon toteutuessa. Tässä tapauksessa se suoritettaisiin heti monitorin suorituksen vapauduttua (olettaen, että suoritettavat tehtävät poimitaan jonosta järjestyksessä).

Aivan lopuksi esitettiin transaktion käsite. Transaktioiden käsite on lainattu suoraan tietokantojen puolelta. Oleellisesti transaktio olettaa, että jo toteutettu tapahtuma voidaan perua. Tämä asettaa joitain rajoitteita sille, mitä voidaan tehdä, mutta toiminee mukavasti, kunhan peruminen on mahdollista. Toisaalta on selvää, ettei kaikkea voi perua. Tässä tapauksessa joutunee käyttämään jotain muuta tekniikkaa. Voisi ajatella esimerkiksi käyttävänsä transaktioiden lisäksi ehdollisia kriittisiä lohkoja tai jotain muuta täydentävää tapaa.

tiistai 3. maaliskuuta 2009

Demo 7 - kommentit

Seitsemännen demokerran tehtävät löytyvät osoitteesta http://users.jyu.fi/~antkaij/opetus/okp/2009/demot/7.html . Tällä kertaa tehtävät olivat vaikeustasoltaan varsin kohtuullisia. Perustehtävistä tein muut paitsi tehtävän 2. Bonustehtäviä en tehnyt tällä kertaa.

2. tehtävä osoitti pätevästi kuinka kompaktisti binääripuita voidaan käsitellä lambda-laskennon avulla. Oma esim. C:llä tai Pythonilla tehty toteutus venyisi varmasti huomattavasti pidemmäksi. Tässä mielessä tehtävä oli hyvä muistutus siitä, että asioita voi tehdä toisinkin varsin kompaktisti.

maanantai 2. maaliskuuta 2009

Luento 15 - Oliokielten erityispiirteitä

Oliot, nuo kaikenkarvaiset pikkukaverit, olivat viikon ainoan luennon aiheena. Sinänsä asia oli jo varsin tuttua, mutta kertauksesta ei ollut haittaa. Samalla tuli opittua muutama uusi asiakin.

Olio-ohjelmointiparadigma on varsin käytetty mutta väärin ymmärretty tapa koostaa ohjelmia. Ei olekaan yllätys, että olio-ohjelmointi on määritelty usein eri tavoin.

Luennolla esitetty Boochin määritelmä oliosta tilan, käyttäytymisen ja identiteetin summana on mielestäni varsin toimiva abstraktilla tasolla. Olio-ohjelmoinnissa on olennaista nimenomaan tämä toisiinsa liittyvien asioiden paketointi samaan kokonaisuuteen eli olioon.

Käsittääkseni olio-ohjelmoinnin lähtökohtana on ollut tarve mallintaa todellisen maailman simuloitavia kohteita. Tätä vartenhan Simulakin kehitettiin. Toki käytännössä mallinnuskohteen ei tarvitse olla konkreettinen, reaalimaailman aspekti. Tässä kohden olio-ohjelmointi voikin muuttua jossain määrin haasteelliseksi. Ongelmaksi muodostuu tilanteeseen sopivan abstraktion löytäminen.

Olio-ohjelmointi voidaan esittää myös abstraktin datatyypin (ADT) ja perinnän summana. Tämä määritelmä jättää kuitenkin huomiotta useita olio-ohjelmoinnissa olennaisia käsitteitä. Taivalsaaren määritelmä, jossa olio-ohjelmointi nähdään olioiden ja summaus (increment)/muokkausoperaatioiden (modification) summana on jo lähempänä sopivaa kompromissia.

Boochin määritelmän (olio=tila+käyttäytyminen+identiteetti) osat ovat sellaisinaan varsin mielenkiintoisia. Tila voidaan rinnastaa attribuutteihin, käyttäytyminen metodeihin ja identiteetti voidaan ajatella esimerkiksi muistiosoitteena, joka osoittaa olion sijaintiin muistissa. Identiteetti voidaan määritellä myös muita tapoja käyttäen.

Periaatteessa identiteetti voisi olla piilotettu attribuutti, joka sisältyy olioon itseensä. Identiteetin tapauksessa oleellista lienee se, että se pysyy konsistenttina koko järjestelmässä koko ajan (toisin sanoen, jos identiteetin arvoa muutetaan, tulee muutoksen heijastua koko järjestelmään) sekä että se on uniikki.

Luennolla esiteltiin ratkaisuja tapaukseen, jossa oliot on hajautettu useisiin eri paikkoihin (hajautettu järjestelmä). Tässä yhteydessä en ryhdy käymään esitettyjä malleja läpi. Mielestäni tässä tapauksessa uniikkiuden vaatimuksesta voidaan kuitenkin hieman joustaa.

Joustaminen on mahdollista siinä tapauksessa, että hajautettuja osia ajatellaan kokonaisuuksina, joille kullekin määritellään oma järjestelmien tasolla uniikki tunniste. Kyseistä tunnistetta käytetään olion oman tunnisteen siinä tapauksessa, kun olion identeetti tulee selvittää. Tällöin kunkin osajärjestelmän oliot voivat sisältää samoja tunnisteita.

Mitä jos olio siirrettäisiin toisesta osajärjestelmästä toiseen? Tässä tapauksessa on mahdollista, että tunnisteet törmäävät. Toisin sanoen kyseisen operaation tapauksessa olion tunnistetta tulee muuttaa. Muutoksen tulee heijastua koko järjestelmään, jotta siinä olisi mitään mieltä.

Käytännössä yllä esitettyä ratkaisutapaa helpompiakin tapoja löytyy. Tarkoituksena oli lähinnä osoittaa, että identiteetin käsitettä voi tarvittaessa osittaa.

Luennolla käytiin läpi myös prototyyppi- ja luokkaperustainen tapa luoda olioita. Luokkaperustainen tapa on ehdottomasti näistä käytetympi. Näenkin sen prototyyppipohjaisen tavan abstraktiona. Luokkien avulla voidaan tarvittaessa toteuttaa prototyyppiperustainen järjestelmä. Näin ainakin Pythonissa. Toki kyseinen temppu vaatii hieman metaohjelmointia, muttei paljon.

Lambda-syntaksina kumpikin tapa näytti varsin yksinkertaiselta. Notaatiota muistutti minua suuresti hajautustauluista avain-arvo pareineen. Ei olekaan tavatonta toteuttaa oliotoiminnallisuutta juuri hajautustauluja käyttäen. Lua on hyvä esimerkki tästä.

Olio-ohjelmoinnin suolana voidaan pitää perintää. Tämä näkyy jo aiemmin mainitusta määritelmästä olio-ohjelmointi=ADT+perintä. Pohjimmiltaan kyse on siitä, kuinka kutsuttuja metodeja etsitään. Perivä olio voi yksinkertaisimmillaan sisältää vain viitteet vanhempiinsa. Käsittääkseni tämä tapa toimii myös moniperinnän tapauksessa, kunhan etsintäjärjestys on sovittu jotenkin järkevästi. Moniperinnän ongelmanahan on se, että samanniminen metodi voidaan määritellä useassa vanhemmassa. Toki myös suorat viitteet käytettävissä oleviin attribuutteihin ja metodeihin toimivat. Tämä tapa tosin syö enemmän muistia kuin suora viite vanhempiolioihin.

perjantai 27. helmikuuta 2009

Demo 6 - kommentit

Kuudennen ja tähän asti vähiten suositun (hiihtolomaviikko :) ) demokerran tehtävät löytyvät osoitteesta http://users.jyu.fi/~antkaij/opetus/okp/2009/demot/6.html . Tällä kertaa tein sekä ensimmäisen ja toisen tehtävän. Toisen tehtävän c-kohtaa "Funktio, joka luo parametrina annetun pituisen Fibonacci-lukujen listan (NumList)." lähdin ratkaisemaan turhan geneerisesti. Ajatuksena oli, että fibonaccin lukujen listaa ajateltaisiin generaattorina ja n-mittainen lista koostettaisiin toisen aliohjelman avulla, jolle annettaisiin syötteenä n:n lisäksi generaattorialiohjelma. Pythonilla ratkaisu menisi seuraavasti:


def fibo():
n, m = 0, 1

while True:
yield n
n, m = m, n + m

def list_factory(n, gen):
return [gen.next() for i in range(n)]

assert list_factory(10, fibo()) == [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]


Tuo varmaan kääntyy myös lambda-lausekkeiksi pienellä vaivalla. Yllä oleva ratkaisu toimii tässä tapauksessa. Kuitenkin yleisemmässä käytössä generaattori varmaan muuttuisi omaksi luokakseen, jonka voisi nollata. Toisaalta fibo generaattoria voisi ajatella myös aliohjelmana, joka laskee n:n arvon tilastaan riippumatta. Isoilla n:n arvoilla aliohjelmapohjaisen ratkaisun suorituskyky saattaisi tosin kärsiä.

torstai 26. helmikuuta 2009

Luento 14 - Parametripolymorfismi

Parametripolymorfismi on eräs Cardellin ja Wegnerin määritelmän kategorioista. Se on määritelty seuraavasti: "Parametric Polymorphism: a polymorphic function has an implicit or explicit type parameter which determines the type of the argument for each application of that function." Oleellista määritelmässä on tyyppiparametrin käsite. Sen avulla voidaan siis määritellä aliohjelman tyyppi. Huomaa, että dynaamisesti tyypitetyissä kielissä tämän voidaan ajatella tapahtuvan implisiittisesti. Staattisesti tyypitetyissä kielissä tyyppiparametri tulee määritellä eksplisiittisesti.

Parametrinen polymorfismi mahdollistaa geneerisemmän koodin kirjoittamisen. Tämä onkin eräs tapa, jolla staattisesti tyypitetyt kielet matkivat dynaamisesti tyypitettyjä. Geneerisen koodin ansiosta voidaan sitä voidaan ajatella abstraktimmalla tasolla.

Tyypitetyn lambda-kielen tapauksessa parametrista polymorfismia voidaan toteuttaa käyttäen ns. Systeemi F -laajennosta, joka luo tyypeille sopivan abstraktion. Käsittääkseni tässä on oleellisesti kyse siitä, että Systeemi F mahdollistaa tyyppien käsittelyn omassa avaruudessaan ja se toteuttaa rajapinnan, jonka avulla se liittyy vanhaan määritelmään.

Systeemi F ei toimi tyyppitoimituksilla (tyyppitasolla toimiva funktio, esim. lista), joten sitä on laajennettu edelleen. Tätä laajennosta kutsutaan nimellä Systeemi F (pikku omega). Tyyppitoimituksia varten tyypeille tulee määritellä tyypit eli luonteet (kinds). Tämä johtunee siitä, että Systeemi F:n tyypit ja tyyppitoimitukset voidaan samaistaa olevan samassa tyyppiavaruudessa.

Luennolla esiteltiin myös varsin mielenkiintoinen foldr (fold right) operaatio, joka tunnetaan myös reduce nimellä. Pythonissa operaatiota voidaan käyttää esim. seuraavasti: reduce(lambda x, y: x*y, [1, 2, 3, 4]). Tässä tapauksessa listan sisältämät luvut kerrotaan keskenään. Tarkemman kuvauksen löydät täältä.

Luento 13 - Subsumptioperiaate

Mitä subsumptio tarkoittaa? Merriam-Websterin mukaan seuraavaa: "to include or place within something larger or more comprehensive : encompass as a subordinate or component element subsumed under the term color". Eli vapaasti tulkittuna kysymys on sisältymissuhteesta. Subsumption avulla voidaan oleellisesti määritellä hierarkioita.

Tästä havainnosta, sisältymisestä, on johdettu subsumptioperiaate, joka vapaasti ilmaistuna sanoo, että tyyppiä voidaan käyttää ylityyppinsä sijalla. Eli esimerkiksi väriin voidaan sijoittaa sen konkreettinen ilmentymä vihreä. Toisaalta pätee myös, että voidaan ottaa alityypin alityyppi, esimerkiksi vihreän sävy jade, ja sijoittaa se ylityyppiinsä väriin. Eli sääntö pätee siis yleisellä tasolla.

Subsumptio pitänee nähdä polymorfismin, eli suomeksi jotakuinkin "monimuotoisuuden", yläkäsitteenä. Tähän liittyen luennolla esiteltiin Cardellin ja Wegnerin esittämä polymorfismin kategorisointi. On huomattava, että polymorfismi ei ole ainoastaan oliokielten ominaisuus vaikka käsite tuleekin oliokielistä puhuttaessa esille usein.

Tämä ilmeni esimerkissä, jossa tarkasteltiin lambda-kieltä ja tietueita. Jotta tietueita voidaan käsitellä lambda-lausekkeiden avulla, kuten intuitio ehkä opastaisi, tulee kielen määrittelyyn tehdä pieniä muutoksia. Osoittautui, että tietuiden käsittelyä voidaan helpottaa nimenomaan yllä esitetyn subsumptiolauseen avulla. Tässä tapauksessa subsumptiota sovelletaan tietueen tasolla. Toisin sanoen tietueesta voidaan karsia lambda-lausekkeen kannalta turha osa pois subsumption nojalla.

Oliokielten ja perinnän tapauksessa subsumptiolla osoittautui olevan kaksi varsin mielenkiintoista ominaisuutta. Olkoon luokat A ja B. B on A:n perillinen. Lisäksi B ylikirjoittaa A:n toteuttaman metodin foo. Voi olla mielekästä, että B saa muuttaa metodin palautusarvon tai parametrien tyyppejä. Kysymys kuuluukin, kuinka tämän tulisi tapahtua.

Subsumptioperiaatteen avulla havaitaan, että palautusarvon tulee olla vähintään alkuperäinen tyyppi tai sen alityyppi. Eli kyseessä on kovarianssi.

Parametrien tapauksessa perivä luokka ei voi rajoittaa vanhempansa määrittelemää tyyppiä, koska tämä sotisi subsumptioperiaatetta vastaan. Tässä tapauksessa tuleekin päteä, että perivän luokan ylikirjoittamien metodien tyyppien tulee olla vähintään alkuperäisiä tyyppejä ja niiden ylityyppejä. Tätä kutsutaan myös kontravarianssiksi.

Eiffelissä parametrien tyyppi on kenties hieman yllättäen toteutettu kovarianssia noudattaen (tekivät ehkä saman intuitiivisen virheen kuin minä alun perin :) ). Tästä seuraa, että tyyppejä joudutaan tarkastelemaan dynaamisesti ohjelman ajonaikana, koska ei voida taata, että parametrin tyyppi on korkeintaan metodin alkuperäisen määritelmän tyyppi. Eiffelin DbC (Design by Contract) on määritelty myös kovarianssin mukaisesti, mikä sinänsä tuntuu loogiselta.

Luennolla mainittiin myös Top ja Bot(tom) tyypeistä, jotka edustavat kaikkien tyyppien ylintä ja alinta tyyppiä. Usein Topin voidaan ajatella olevan ohjelmointikielen ns. Object-luokka. Bot edustaa pohjaa ja sitä voidaan ajatella myös noreturn tyyppinä, joka lopulta terminoi ohjelman suorituksen.

keskiviikko 18. helmikuuta 2009

Demo 5 - kommentit

Viidennen demokerran tehtävät löytyvät osoitteesta http://users.jyu.fi/~antkaij/opetus/okp/2009/demot/d5.pdf . Demotehtävät olivat jälleen kerran lambda painotteisia. Teinkin ainoastaan bonustehtävien ensimmäisen osan, jossa tuli suunnitella yleistajuinen esitys aliohjelmien toteuttamiselle tyypitettyyn while-kieleen. Tehtävään pääsi mukavasti kiinni pohtimalla hieman, miten asia on hoidettu esimerkiksi C:ssä ja Javassa. Myös näkyvyysalueita (sulkeumat) sai miettiä hieman.

Luento 12 - Rekursiiviset tyypit

Tällä kertaa käsiteltiin rekursiivisia tyyppejä, jotka lienevät useille tuttuja esimerkiksi C:n linkitettyjen listojen toteutuksesta. Ajatuksena on, että tietue voi viitata itseensä. Täten voidaan luoda mitä mielenkiintoisimpia rakenteita. Toki ilman rekursiota pärjää ja on pärjättykin, mutta mikäli ohjelmointikieli sallii sen käytön, sitä voi kannattaa käyttää.

Luennon aluksi käsiteltiin myös tietuieiden samuutta. Helpoimmalla pääsee vertailemalla tietuiden nimiä (ns. nimisamuus). Toisaalta tässä tapauksessa kielen tasolla tulee huolehtia siitä, että nimet ovat uniikkeja tietuieita käytettäessä. Toinen tapa käsitellä samuutta on jättää nimen käsite omaan arvoonsa ja vertailla puhtaasti tietueiden sisältöä.

Tässä tapauksessa voidaan kiinnittää lisäksi huomiota tietueen sisäiseen järjestykseen tai jättää se huomiotta. Luennolla esitettiin edellä mainittuun tapaan bisimulaatioon pohjautuva ratkaisu, jossa vertailtavia tietueita askelletaan samanaikaisesti ja tutkitaan onko kunkin vertailtava tieto sama sen nimen ja tiedon suhteen. Tätä ehtoa voisi varmaan löyhentää keskittymällä vain tietotyyppiin ja jättämällä nimen huomiotta. Tosin mikäli nimi jätetään huomiotta voivat semanttisesti hyvinkin erilaiset tietueet tulla tulkituiksi samoiksi! Käsittääkseni jälkimmäisessä järjestyksen huomiotta jättävässä tapauksessa bisimulaatio voidaan toteuttaa käyttämällä toista tietuetta referenssinä ja toista tutkittavana. Huomaa, että kummankin tietueen tulee vuorollaan toimia referenssinä, jotta voidaan taata ettei toisessa tietueessa satu olemaan ylimääräisiä alkioita! Seuraavana algoritmi pseudokoodina:


struct1, struct2 = Struct(), Struct()

def structs_are_equal(struct1, struct2):
for ref_node in struct1:
found_node = False

for node in struct2:
if str(ref_node) == str(node)
and type(ref_node) == type(node):
found_node = True
break

if not found_node:
return False

return True # tietueet ovat samoja


Sinänsä algoritmissa ei ole mitään ihmeellistä (ellen sattunut tekemään jotain radikaalia virhettä). Luennolla esitettiin myös rekursiivisten tyyppien toteutusta lambda-kielessä. Koko touhun ytimessä on kiintopisteoperaattori, johon määritelty tyyppi voi viitata mahdollistaen rekursion. Toinen varsin mielenkiintoinen sivuttu asia oli laskostaminen (isorekursio), jossa kiintopiste irrotetaan erikseen kokonaisuudesta ja rekursio mahdollistuu erityisten laskostamisaliohjelmien (fold, unfold) avulla, joilla tietue käsittääkseni voidaan kääriä auki (unfold, rekursio pois) ja kääriä kiinni (fold, rekursio takaisin).

Myös tietueilla operoimista käytiin läpi. Tässä yhteydessä huomioni herätti erityisesti jo Pythonista tuttujen sum ja lambda aliohjelmien toteutus. "reduce" lienee menee hieman samaan tapaan. Ajatuksena näissä pienissä apureissa on, että annettua tietuetta (tässä tapauksessa rajoituttiin NumListiin (numeroiden lista)) voidaan manipuloida annetun aliohjelman avulla. Esimerkiksi map suorittaa annetun aliohjelman kaikille tietueen alkioille ja palauttaa muunnetun tietueen.

Myös virtojen (stream) käsite kiinnitti huomioni. Virralta voidaan kysyä arvoja, kuten generaattorilta ikään. Virta voi palauttaa yksinkertaisimmillaan jonkin luvun. Se voi myös säilyttää tilansa mahdollistaen matemaattisten funktioiden kuvaamisen virtana. Myös satunnaislukugeneraattorinkin luulisi syntyvän helposti tätä kautta.

maanantai 16. helmikuuta 2009

Luento 11 - Tyyppijärjestelmän laajennoksia

Luento rakensi aiemmin esitetyn teorian luomalle pohjalle. Aluksi esitettiin mielenkiintoinen huomio siitä, kuinka C:stä tuttu void tyyppi on määritelty kielen spesifikaatiossa. Sen mukaan void on tyyppi, jolla ei ole yhtään arvoa. Aliohjelmakutsun tapauksessa tämä tarkoittaa intuitiivisesti hieman hämärää käyttäytymistä.

Olkoon void tee_jotain(int a, int b) { ... }. C käyttäytyy siten, että aliohjelmakutsu (tee_jotain(4, 6)) palaa kutsuvaan funktioon. Kuitenkin määritelmää tarkasti lukien voisi olettaa, että paluuta ei tapahdu. Luennoitsija esittikin, että tarkempi määritelmä olisikin, että void palauttaisi pohjan (pohja on käsittääkseni jotakuinkin sama asia kuin, että aliohjelman saavuttaessa loppunsa, se palaa kutsuvaan käyttäen paluuosoitetta). Edellä mainitun epäselvyyden vuoksi esitettiin erityistä "noreturn" tyyppiä, joka ei palaa kutsujaan.

En ole varma olisiko noreturn tyypistä käytännön hyötyä. Sinällään sen perusajatusta voi helposti emuloida esimerkiksi kutsumalla järjestelmätason poistukomentoa. Toisaalta se olisi hyvinkin eksplisiittinen tapa ilmaista, että aliohjelmakutsu ei palaa takaisin.

Luennolla mainittiin myös, että joissain kielissä aliohjelmista voidaan erottaa erityiset proseduurit. Näistä olen kuullut aiemminkin (erityisesti Delphin yhteydessä), mutta merkitys oli päässyt hieman unohtumaan. Ajatuksena on, että proseduurilla ei ole lainkaan palautusarvoja missään tapauksessa. Proseduureja käytetään sivuvaikutuksien avulla ohjelmointiin. Proseduuri voi manipuloida esimerkiksi jotain globaalia tilaa.

Periaatteessa olisi mahdollista luopua aliohjelman käsitteestä ja käyttää ainoastaan proseduureja. Kuitenkin pelkästään sivuvaikutusten avulla ohjelmointi tuntuu jotenkin vieraalta ajatukselta. Miten tässä tapauksessa kannattaisi hoitaa esimerkiksi hypot(a, b) (hypotenuusa) tulos? Tuloksen voisi tallentaa johonkin globaalisti saatavilla olevaan paikkaan, vaikkapa globaaliin result muuttujaan. Tämän ratkaisumallin ongelmat tulevat esiin, kun pitää suorittaa useita samanaikaisia proseduureja, jotka manipuloivat samaa globaalia dataa. Ei ole taetta siitä, että tulos on oikea. Toisessa ratkaisumallissa voitaisiin palautusosoite antaa parametrina.

Mielestäni aliohjelmien käyttäminen pelkkien proseduurien sijasta on varsin perusteltua. Toisaalta proseduuri sinällään sisältyy aliohjelman määritelmään, mikäli aiemmin mainittu void tai vastaava konstruktio on käytössä. Tässä mielessä on varsin luontevaa, ettei kieli satu sisältämään erillistä proseduurin käsitettä.

Mikäli kieleen haluaisi tarkan jaon aliohjelmien ja proseduurien välille, tämän voisi tehdä siten, että ainoastaan proseduureilla voi olla sivuvaikutuksia. Toisin sanoen voitaisiin eksplisiittisesti kieltää aliohjelmien sivuvaikutukset. Ohjelmoijan kannalta tästä saattaisi olla enemmän haittaa kuin hyötyä joustavuuden suhteen. Kuitenkin semantiikka säilyisi erittäin selvänä.

Aliohjelmista puhuttaessa eräs mielenkiintoinen ja luennolla sivuttu seikka on se, kuinka ne voivat palauttaa monia arvoja. Aina ei ole mielekästä palauttaa vain yhtä arvoa. Toisaalta, jos aliohjelma voi palauttaa monia arvoja kerralla, kuinka se tulisi määritellä staattisesti tyypitetyssä kielessä? Eräs luonteva tapa tehdä tämä on palauttaa monikko (tuple) tai tietue (structure).

Sekä monikot ja tietueet olivat jo ennalta tuttuja. Monikoita olen käyttänyt varsinkin Pythonissa. Sen toteutuksessa monikon sisältämiä viitteitä olioihin ei voi enää muokata luonnin jälkeen. Huomaa, ettei tämä tarkoita etteikö olioiden sisältöä voisi muokata! Ainoastaan viitteet on "lukittu".

Tietue on varsin tuttu käsite C:stä. Tietuehan on pohjimmiltaan vain monikko, jossa sisältöön voidaan viitata konkreettisten nimien avulla pelkkien indeksien sijasta. Tietueisiin liittyy myös varianttien käsite, jossa tietueen tiettyä osaa voidaan varioida. Voidaan siis matkia yksinkertaista perintää noin tietueen tasolla.

Tietue muistuttaa hyvin paljon assosiatiivista listaa (dictionary) rakenteeltaan. Dynaamisesti tyypitetyissä kielissä assosiatiivisen listan avulla voidaankin emuloida tietueita. Esimerkiksi Luassa tässä mennään varsin pitkälle erityisesti syntaksin suhteen (syntaksi muistuttaa C:n tietuesyntaksia mutta operoi assosiatiivista listaa).

Luennon lopussa käytiin läpi vielä viitetyyppien käsitettä. Joskus voi olla hyvinkin hyödyllistä, että voidaan operoida muistiosoitteiden tarkkuudella, kuten C:ssä ikään. Korkeamman tason kielet abstrahoivat tämän käyttäytymisen syntaksinsa alle. Tässä tapauksessa joudutaan tekemään automaattia käännöksiä osoitteiden ja arvojen välillä.

keskiviikko 11. helmikuuta 2009

Demo 4 - kommentit

Neljännen, ja tähän asti vähiten suosituimman, demokerran tehtävät löytyvät osoitteesta http://users.jyu.fi/~antkaij/opetus/okp/2009/demot/4.html . Tehtävät olivat haastavia erityisesti mikäli lambda-laskento oli vielä hieman hakusessa. Itse sain tehtyä ensimmäinen tehtävän, josta sain jopa osan oikein.

Tehtävät olivat varmasti helppoja jos tiesi mitä on tekemässä. Ehkä suurin ongelma näin lambda-noobin kannalta on se, että en ole yksinkertaisesti nähnyt tarpeeksi lambda-lausekkeita. Mielestäni koodia tulisi lukea, kuten kirjallisuutta ikään (Milloin yliopistolle tulee tarjolle koodin lukukurssi?). Tällä tavoin saattaa oppia erilaisia tapoja ratkaista erinäisiä ongelmia ja samalla saattaa oppia erottamaan hyvän koodin huonosta. Tiedä sitten voiko "huonoa" lambda-koodia edes kirjoittaa, kunhan se toimii oikein?

Demokerran lopulla käytiin läpi pieni bonustehtävä. Ajatuksena oli luoda binäärilaskuri (0 -> 10 -> 11 -> 100 -> 101, ...) käyttäen lambda-laskentoa. Laskurille vietiin binäärinen arvo listana ja se palautti sitä seuraavan arvon listana kuitenkin siten, että siihen oli lisätty yksi (siis kymmenjärjestelmässä, ei concat-operaationa).

Sinänsä tehtävän formulointi algoritmiksi on varsin helppoa. Haastavuus piilikin algoritmin muuttamisessa lambda-muotoon, mikä loppujen lopuksi osoittautui melko yksinkertaiseksi.

Luento 10 - Lisää tyyppiteoriaa

Tällä kertaa pidettiin erittäin matemaattisesti orientoinut luento. Olennaisesti esitettiin staattisten tyyppijärjestelmien teoreettista perustaa. Tässä yhteydessä en aio käydä todistuksia läpi ja tästä tulleekin lyhin postaus tähän asti kirjoitetuista.

Olennaisin kysymys tämän postauksen kannalta lieneekin mitä opin luennolla, onhan kyseessä oppimispäiväkirja. Intuitiivisesti voi tuntua selvältä, että vahvasti tyypitetty kieli ei tuota ajonaikaisia (dynaamisia) virheitä vaan kaikki tyyppivirheet saadaan kiinni käännösvaiheessa. Kuitenkin asian todistaminen ei ole aivan vaivatonta, kuten luennolla näin.

Kenties tähän asti mystisin esitetty asia oli Hindleyn ja Milnerin tyyppipäättely. Tässä tapauksessa tyypit nimenomaan päätellään niiden käyttökontekstin perusteella.

Luennolla mainittiin myös Curry-Howard vastaavuudesta (Curry-Howard correspondence), jonka ajatuksena on, että ohjelmoinnista tuttuja tyyppejä ja ohjelmia vastaavat matematiikan puolella väittämät ja todistukset. Ohjelmointi on jossain mielessä matematiikkaa ja todistaminen taas ohjelmointia. Tästä varmaan seuraa myös, että matematiikkaa voidaan muuntaa ohjelmiksi ja toisinpäin. Toisin sanoen koodin toimivuus voidaan todistaa, mikä sinällään on ihan järkeenkäypää. Vaikeampi kysymys onkin onko mahdollista kirjoittaa kääntäjä, joka muuttaisi todistuksia haluttuun kieleen ja toisinpäin.

Todistuksista voisin mainita vielä sen verran, että äkkiseltään katsottuna ne näyttävät erittäin vaikeaselkoisilta. Tämä kuitenkin johtuu vain niiden korkeasta abstraktiotasosta. Matemaattinen notaatio on erittäin tiivistä. Luentojen välisellä tauolla kävinkin keskustelua notaation kääntämisestä konkreettiseksi suomeksi, mikä sinällään madalti todistusten ymmärtämisen kynnystä. Asiat ymmärtäisi ehkä kenties vielä paremmin, jos oikeasti joutuisi rakentamaan niihin pohjautuvia kieliä.

maanantai 9. helmikuuta 2009

Luento 9 - Tyyppiteorian alkeet

Yhdeksännen luennon aiheena oli tyyppiteoria. Kaikissa ohjelmointikielissä on tyyppejä. Tyyppi kertoo oleellisesti, mitä tietyllä oliolla voi tehdä. Jossain ohjelmointikielessä voi olla esimerkiksi sallittua yhdistää lukuja tyyliin "5 . 2 . 342", josta seuraisi luku "52342". Toisaalta tämän tyylinen operaatio voi aiheuttaa virheen. Virhe voidaan havaita joko käännösvaiheessa tai ajonaikana. Tässä palataan jälleen aiemmin mainittuun jakoon, dynaamisesti ja staattisesti tyypitettyihin kieliin.

Kielet eivät välttämättä ole selvästi ainoastaan staattisia tai vain dynaamisia vaan ne voivat sisältää myös molempia piirteitä. Esimerkiksi Javassa käännösaikana saadaan kiinni melko paljon erilaisia tyyppivirheitä. Toisaalta myös ajonaikana niitä voidaan havaita. Pythonin tapauksessa staattista, käännösaikana tapahtuvaa tyyppitarkistusta ei tehdä lainkaan. Tarkistukset tehdään dynaamisesti ajonaikana.

Tässä yhteydessä lienee tarpeen mainita vahvuuden käsitteestä. Javan staattista ja dynaamista tyyppitarkistusta kumpaakin voidaan pitää melko vahvana. Pythonin staattista tarkistusta voidaan pitää erittäin heikkona, koska sillä ei ole sitä lainkaan, mutta toisaalta sen dynaaminen tarkistus on erittäin vahva.

Itselleni Pythonin lähestymistapa on Javaa luontevampi. Tyyppejä voi halutessaan välillä tarkistaa (esim. assert toimii mukavasti), mutta itse kieli ei pakota määrittelemään eksplisiittisiä tyyppejä. Tyypit määräytyvät sijoituksen perusteella. Esimerkiksi jos sijoitan muuttujaan kokonaisluvun, on sen tyyppi sen jälkeen kokonaisluku. Toisaalta myös kaikki muuttujat viittaavat olioihin (primitiivityyppejä ei ole lainkaan, kuten Javassa), mikä sinällään on ihan mielenkiintoista. Tietenkin myös aliohjelmia voi sijoittaa ovathan ne olioita.

Javan tapauksessa tyyppejä joutuu miettimään tarkemmin. Toisaalta myös geneerisen koodin kirjoittaminen on hankalampaa, mutta tätä varten Javassa onkin omat vippaskonstinsa. Dynaamisesta ympäristöstä tuttua tapaa ajatella on ikäänkuin liimattu Javan päälle, mikä ei välttämättä ole aivan huono asia, geneerisyys kun sattuu olemaan varsin kätevä apuväline.

Näen tyypit osana testausta. Koska dynaamisesti tyypitettyjen kielten tapauksessa tyyppejä ei kirjoiteta eksplisiittisesti itse ohjelmakoodiin, kannattaa esimerkiksi aliohjelmien ja luokkien rajapinta dokumentoida hyvin esimerkiksi testien ja docstringien avulla. Toki näin kannattaa tehdä myös Javan tapauksessa tyypeistä huolimatta.

Luennolla mainittiin, että staattisesti tyypitetyt kielet tulevat ennemmin tai myöhemmin kehittäjän tielle. Kielten rajoituksia voi kiertää mutta pitemmän päälle näin saa aikaan vain sotkua. Kielen kehittäjän näkökulmasta onkin haaste luoda staattisesti tyypitetty kieli, joka hyppii mahdollisimman vähän kehittäjän silmille.

Nähdäkseni staattisesti tyypitetyt kielet voivat toimia hyvin matalamman tason kielinä (vrt. C). Dynaaminen tyypitys sen sijaan toimii paremmin korkeamman abstraktiotason sisältävissä kielissä. Staattisen tyypityksen tapauksessa teknisiä, matalamman tason asioita tulee pohdittua jo luonnostaan enemmän. Dynaamisen tyypityksen tapauksessa teknisen tason jättää mielellään vähemmälle huomiolle.

Kielessä tulee olla tyyppejä joka tapauksessa. Hyvä kysymys onkin, kuinka monta tyyppiä kielessä tulee olla. Tämä riippuu suoraan kielen käyttötarkoituksesta. Mikäli ollaan tekemisissä matalamman tason kielen kanssa, voi olla hyvinkin suotavaa, että kielen tyypit on tarkasti mietitty nimenomaan suorituskyvyn kannalta. Korkeamman tason kielessä abstraktiota voidaan viedä suorituskyvyn kustannuksella korkeammalle tasolle. Ääriesimerkkinä Lua sisältää vain yhden lukutyypin, joka on toteutettu doublena. Perinteistä kokonaislukutyyppiä kielessä ei ole siis lainkaan. Toisaalta esimerkiksi C sisältää laajan kokoelman eri lukutyyppejä.

Luennolla esitettiin myös formaaleja tapoja esittää tyypillinen (siis kieli, joka sisältää tyyppejä) kieli. Oleellista määritelmissä on se, että niiden avulla muodostaa helposti ohjelmakoodiksi muuttuva rajoittimien joukko, jonka avulla tyyppeihin liittyvät tarkistukset voidaan tehdä. Rajoittimet voidaan esittää loogisina päättelylauseina, jotka olivatkin jo tuttuja aiemmalta luennolta. Ajatuksena on, että tietyllä ehdolla voidaan tehdä jotain. Karkeasti yksinkertaistettuna summa 5+8 voidaan esittää sääntönä, joka tarkistaa, että summa muodostuu tekijöistä, joiden kummankin tyyppi on luku.

Staattiset tyyppijärjestelmät vaikuttavat varsin yksinkertaisilta näin äkkiseltään. Kompleksisuus lienee syntyykin siinä vaiheessa, kun pitää määritellä useita eri tyyppejä ja miettiä mahdollisia eri operaatioita, joiden avulla tyypitettyjä muuttujia sallitaan käsiteltävän.

tiistai 3. helmikuuta 2009

Demo 3 - kommentit

Väki vähenee ja pidot paranee. Kolmannen demokerran tehtävät löytyvät osoitteesta http://users.jyu.fi/~antkaij/opetus/okp/2009/demot/3.html . Ensimmäinen tehtävä seuraili jo edellisistä demoista tuttua sanaselityskonseptia. Noista kiinnostunut kaivaa määritelmät pikaisesti vaikka Wikipediasta. :)

Toisessa tehtävässä päästiin pohtimaan Jensenin vekotinta. Jonkinmoisena johtopäätöksenä tehtävään liittyen todettakoon, että moderneissa ohjelmointikielissä samaan ja semanttisesti selkeämpään lopputulokseen pääsee esimerkiksi lambdoja käyttäen (lambda kuvaa matemaattisen funktion, mikä Jensenin versiossa seuraa tuota call-by-name ajatusta). Merkitsin sekä ensimmäisen ja toisen tehtävän tehdyiksi. Kolmannen ja neljännen jätin väliin ja niiden sijaan teinkin bonustehtävän.

Yllättäen kolmannen tehtävän oli tehnyt vain yksi demojen osanottaja. Toistaalta "tarkasti laskeminen" on teknistä puuhaa, kuten osoittautui. Neljättä tehtävää ei ollut tehnyt kukaan, joten se jäi ikäänkuin laiskanläksyksi. Toisaalta tehtävänannon pyytämän for-silmukan johtanee jossain määrin whilen määrittelystä. Suurin haaste tuossa lienee iteraatiologiikan sisällyttäminen itse lausekkeeseen jotenkin järkevällä tavalla.

Bonustehtävänä oli tällä kertaa while-kielen denotationaaliseen määritelmään pohjautuvan tulkin kirjoittaminen. Valitettavasti olin ainut, joka oli kyseisen tehtävän tekemään, joten en saanut kyhäelmälleni vertailukohtaa (tosin hieman kuuklaamalla noita varmaan löytyy). Esittämäni versio löytyy osoitteesta http://users.jyu.fi/~jutuveps/interpreter.py .

Kyseessä oli ensimmäinen kirjoittamani tulkki. Siihen nähden tuo meni ihan mukavasti. Toteutin sen interaktiiviseen tyyliin, joten käyttäjä näkee reaaliajassa mitä tulkki oikein puuhailee.

Toisaalta tuossa versiossa on muutamia kohtia joita voisi huomattavasti hioa. Näin jälkikäteen ajatellen olisi kannattanut muutama viikko sitten kirjoittamani AST (Abstract Syntax Tree) luokka naftaliinista ja hoitaa sijoitusoperaatio sen kautta. Mutta meneehän se raa'alla voimallakin. :)

Vertailuoperaattorin kohdalla itse perusajatus on nähdäkseni ihan hyvä. Tosin check_condition funktion toteutusta voisi tiivistää aavistuksen muuttamalla sen taulukkopohjaiseksi ja tekemällä vertailufunktioista lambdoja. Tuo nyt on vain pieni kauneusvirhe mutta jotain mitä tekisin toisin.

Puolipisteen käsittely on jossain määrin karmealla tavalla toteutettu. Tällä hetkellä se toimii vain yksinkertaisimmassa tapauksessaan (esim. a = 5; b = 8). Tuotakin voisi hieman miettiä uudelleen.

Tulkin kirjoittaminen ei ollut läheskään yhtä vaikeaa kuin ennalta odotin. Se sopii mukavaksi ohjelmointiharjoitukseksi, jos sattuu tylsyys yllättämään. Jos oikein aikaa riittää, voi kenties saada aikaan jotain LOLPythonin tapaista.

KTHXBYE

Luento 8 - Aliohjelmien formalisointia - lambda-kieli

Kahdeksannen luennon aiheena oli käsitteen tasolla jo jossain määrin tuttu lambda-kieli. Lambdoja, anonyymeja funktioita, onkin aina silloin tällöin tullut käytettyä. Perusajatuksena on, että lambdalle annetaan syötearvoja, joiden avulla operoidaan ja lopulta palautetaan jotain järkevää. Esimerkiksi Pythonin syntaksia mukaillen "hypot = lambda x, y: (x*x + y*y)**0.5" toimisi hypotenuusan laskemiseen (hypot(3, 4) palauttaa 5 jne.). Lambdat ovat erityisen päteviä template method suunnittelumallin kanssa hyödynnettyinä.

Koko lambda-laskento perustuu sisäkkäisten funktioiden periaatteeseen. Sinänsä lambda-kielessä ei ole kokonaislukuja, liukulukuja ja muita vastaavia mukavuuksia! Ne tulee määritellä kielen itsensä avulla. Tämä sinällään tuntuu oudolta hieman erilaiseen määrittelyyn tottuneelta. Funktioiden sisäkkäisyydestä seuraa myös luonnostaan ohjelmoinnissa tuiki oleellisten näkyvyysalueiden käsite.

Lambda-lausekkeita voidaan käsitellä erityisten muunnosten avulla. Käsittely on oleellista erityisesti lausekkeiden arvioinnin kannalta. Täysin normalisoitu lambda-lauseke edustaa vastausta. Normalisoinnissa ajatuksena näyttää olevan oleellisestikin juuri lausekkeen pienten osien arviointi kerrallaan.

Alkeellisena esimerkkinä voisi ajatella kahden positiivisen kokonaisluvun n ja m summausta. Käsittääkseni tässä tapauksessa luentomonisteen lyhennysmerkintöjä lainaten saatettaisiin päätyä kenties seuraavanlaiseen normalisoituun muotoon: add(succ(succ(...(succ(zero)))), (näitä on n-kappaletta), succ(succ(...(succ(zero)))) (näitä on m-kappaletta)). Kokonaisluvut koostetaan summaamalla nollaan (zero) luvun arvo kertaa yksi ja lopuksi nämä luvut lasketaan yhteen.

Kertolaskulyhennyksen voisi mielestäni johtaa add:stä rekursiivisesti kenties seuraavasti (kerrotaan luvut n ja m, molemmat positiivisia kokonaislukuja): Olkoon positiviinen kokonaisluku int(n) = succ(succ(...(succ(zero))), jossa succ toistuu n kertaa (int on helpompi lukea :) ). Nyt saadaan add(int(n), add(int(n), add(int(n), ...*))), jossa * tarkoittaa, että sisäkkäisiä n operaatioita suoritetaan m kertaa (kertolaskun tarvitsema määrä). Veikkaisin, että tämä on jokseenkin sama kuin luentomonisteessa esitetty versio (pitää vielä tajuta, miten noita lambdoja lukea kunnolla :) ).

Liukulukujen (esim. 1.4235) esittäminen lambda-muodossa lienee haasteellista. Erittäin alkeellinen ja kenties harhaoppinen tapa olisi hahmottaa se kokonaislukuna siten, että desimaalierottimen sijainti suhteessa luvun vasemmanpuoleisimpaan tekijään tiedetään jotenkin erikseen. Toisaalta voisi kysyä myös, miten esittää esim. sqrt(2) lambda-notaation avulla. Tämä lienee tarvitsee jonkinmoisen pätevän määritelmän neliöjuurelle ja tätä kautta pääsisi ainakin rajattuun joukkoon liukulukuja käsiksi.

Tällä hetkellä suurin haaste lienee lukutaidon oppiminen. Pitänee vilkaista jotain funktionaalista kieltä ja katsoa miltä asiat näyttävät sen kannalta. Asia ei ole mahdotonta mutta tarvitsee hieman sulattelua. :)

maanantai 2. helmikuuta 2009

Luento 7 - Lyhytaskelsemantiikka

Seitsemännen luennon aiheena oli lyhytaskelsemantiikka. Aiemmin esitellyssä denotationaalisessa semantiikassa päädyttiin pieniin vaikeuksiin jo while-lauseketta määriteltäessä. Kehittyneempiä käsitteitä on huomattavasti vaativampaa määritellä.

Lyhytaskelsemantiikan ajatuksena on luoda koneläheisempi tapa määritellä semantiikkaa. Denotationaalisen lähestymistavan perusajatushan oli olla täysin koneesta riippumaton, yleinen tapa. Lyhytaskelsemantiikka ottaa askelen lähemmäksi tietokonetta ja luopuu yleisyyden vaatimuksesta. Se sisältää tietokoneen abstraktion ja sen avulla määritellään omaa konekieltä, jolla sen tilaa manipuloidaan. Tämä katsantokannan muutos mahdollistaa helpommin hallittavan semantiikan hahmottamisen. Kääntöpuolena mainittiin todistamisen hankaloituminen.

Denotationaaliseen semantiikkaan verrattuna lyhytaskelsemantiikka on huomattavasti tiiviimpää ja siten myös mielestäni luettavampaa. Myös tapa, jolla ohjelmia voi tulkata lyhytaskelsemantiikan avulla annetun määritelmän avulla vaikutti varsin pätevältä.

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.