Erlang
1. Johdanto Erlang on funktionaalinen ja rinnakkainen ohjelmointikieli. Se kehitettiin alun perin 1980- luvulla Ericssonilla puhelinvaihteiden ohjelmistoja varten ja se on suunniteltu niiden vaatimuksia, kuten hajauttamista, virhesietoisuutta ja saavutettavuutta silmällä pitäen. Vuodesta 1998 lähtien se on ollut avoimen lähdekoodin projekti. Vaikka Erlang onkin yleiskäyttöinen kieli, monista sen ominaisuuksista paistaa läpi, se että se on oikeasti suunniteltu varsin tarkkaan määriteltyä tarkoitusta varten, eikä välttämättä oikeasti sovellu moniin muihin asioihin. Ensimmäiset toteutukset Erlangista tehtiin Prologilla mutta sen osoittauduttua liian hitaaksi vaihdettiin toteutuskieleksi C. 2. Ominaisuudet 2.1. Tietotyypit ja tyypitys Erlang on dynaamisesti ja vahvasti tyypitetty kieli. Dynaamisen tyypityksen vuoksi muuttujien tyyppejä ei tarvitse ilmoittaa erikseen. Esimerkiksi hahmontunnistus hyväksyy useimmissa tapauksissa kaikenlaisia tietotyyppejä ja niiden yhdistelmiä ja tunnistaa ne automaattisesti. Erlang on kuitenkin myös vahvasti tyypitetty, eli se ei suorita implisiittisiä tyyppimuunnoksia, ja jotkut operaatiot, kuten yhteenlasku esimerkiksi numeron ja atomin välillä, johtavat virheeseen. Eksplisiittiset tyyppimuunnokset ovat kuitenkin sallittuja. 2.1.1. Perustietotyypit Erlang tukee sekä kokonaislukuja että liukulukuja, eikä tee eroa niiden välille. Niitä voi laskea yhteen, ja kertoa ja jakaa vapaasti. Kokonaislukuja käsitellään siis käytännössä liukulukuina aritmeettisissa operaatioissa. Kokonaislukujen väliselle jakolaskulle ja modulo-operaattorille on omat avainsanansa, div ja rem. Erlangista löytyy myös boolean-tietotyyppi. Epätavallisempi Erlangista löytyvä tietotyyppi on atomi (atom). Atomit ovat vakioita, joiden nimi on myös niiden arvo. Atomi foo on foo eikä sille voi tehdä mitään operaatioita. Tämä saattaa vaikuttaa hyödyttömältä ja epäintuitiiviselta, mutta atomeita voi käyttää vakioina ja enumeraation kaltaisina arvoina. Niitä kannattaa käyttää myös tilanteissa, joissa dataan tavallisesti liitettäisiin jokin merkkijono kertomaan mitä data tarkoittaa, esimerkiksi {50, kg }. Tietoliikennetaustansa vuoksi Erlangissa on laaja tuki erilaisille bittioperaatioille ja -asetuksille, kuten sille, onko eniten merkitsevä bitti ensimmäinen vai viimeinen. Tyypillisten loogisten ja shift-operaatioiden lisäksi myös hahmontunnistus (pattern matching) toimii binääridatalle; esimerkiksi <<A:16, B:8>> = <<255,0,170,>> tuottaa lopputuloksen A = <<255,0>> ja B = 170. Kukin listattu arvo on yksi tavu ja muuttujien perässä olevat numerot tarkoittavat haluttujen bittien määrää. Myös listaoperaatiot ovat kaikki käytettävissä binääridatalle.
Merkkijonoille Erlangissa ei ole omaa tietotyyppiä vaan niitä käsitellään listoina. Tuki merkkijonoihin liittyville operaatioille ei siten ole myöskään kovin hyvä. 2.1.2. Tietorakenteet Erlang tarjoaa kaksi perustietorakennetta: listan ja tuplen. Kumpikin voi sisältää mitä tahansa tietotyyppejä, mutta tuple on vakiomittainen ja listan pituus voi muuttua. Listat toimivat hyvin samalla tavalla kuin esimerkiksi Haskellissa. Listan ensimmäisen alkion käsittely ja alkion lisääminen listan alkuun ovat kevyitä operaatioita, ja listan lopun käsittely on hitaampaa. Listojen käsittelyyn on tarjolla paljon erilaisia hahmontunnistusmalleja, joilla voi luoda uusia listoja tyhjästä tai toisen listan perusteella. Toisin kuin monissa muissa kielissä, tuplen alkioita ei voi käsitellä first- ja second-operaattoreilla tai vastaavilla mekanismeilla, vaan ne täytyy sijoittaa muuttujiin hahmontunnistuksen avulla. Esimerkiksi käskyn {X, Y} = {1, 2} lopputuloksena X = 1 ja Y = 2, mikäli X ja Y olivat alustamattomia muuttujia. Record-tyyppi sallii structin kaltaisten rakenteiden luonnin, joiden atribuutteja voi käsitellä suoraan määritellyn nimen kautta. Käytännössä record-tyypin perustana on kuitenkin vain joukko sisäkkäisiä tupleja, ja tuplejen avulla saakin aikaan samanlaisen toiminnallisuuden. Record ei ole structiin tai luokkaan verrattava tietotyyppi, joten sen käsittely on monella tapaa kömpelömpää kuin esimerkiksi C-structin. Muista tietorakenteista löytyy useita hieman erilaisia versioita, koska suunnittelustrategiana on ollut, että ei ole vain yhtä universaalisti parasta tapaa toteuttaa tietynlainen tietorakenne. Dictionaryn kaltaisia tietotyppejä on neljä: Pienille tietomäärille on olemassa proplist ja järjestetty orddict, ja suuremmille tietomäärille dict ja gb_tree (general balanced tree). Myös joukoille on erilaisia toteutuksia, joista jokaisella on omat hyvät ja huonot puolensa. Esimerkiksi sets-moduuli tarjoaa tehokkaammat lukuoperaatiot ja gb_sets taas nopeammat kirjoitusoperaatiot. Erlangissa on myös sisäänrakennettuna muite tietorakentaita, kuten suunnattu graafi. O(1)-ajassa toimivia taulukoita Erlangissa ei kuitenkaan ole. 2.1.3. Muistinhallinta Erlangissa muistinhallinta on automatisoitu. Ohjelmoijan ei tarvitse varata tai vapauttaa muistia itse, vaan se on Erlangin virtuaalikoneen vastuulla. Erlang käyttää myös roskienkeruuta. Erlangissa kullakin prosessilla on oma keko ja pino, jotka kasvavat toisiaan kohti. Kun ne kohtaavat, roskienkeruu aktivoituu ja vapauttaa muistia. Jos roskien kerääjä ei pysty vapauttamaan tarpeeksi muistia, kekoa kasvatetaan. Roskienkeruu ei kuitenkaan poista muistista atomeita, ja siksi niitä ei kannata luoda dynaamisesti tai muutenkaan kovin montaa.
2.2. Funktionaalisuus Erlang on funktionaalinen kieli, ja funktioita voi siinä käyttää kuten muitakin arvoja, esimerkiksi parametrina tai paluuarvona. Kieli ei salli arvojen mutatoimista eikä siinä myöskään ole minkäänlaisia silmukkarakenteita, vaan kaikki silmukat on toteutettava rekursiolla. Erlang ei kuitenkaan ole puhdas funktionaalinen kieli kuten esimerkiksi Haskell. Erlang sallii viitteiden läpinäkyvyyden rikkomisen tarvittaessa, eli funktio saa palauttaa samoilla parametreilla erilaisia tuloksia. Esimerkiksi päivämäärän palauttava funktio saa palauttaa joka päivä eri päivämäärän. Erlang ei myöskään tue curry-muunnoksia eli useamman muuttujan funktion muuntamista ketjuksi yhden muuttujan funktioita. Tämän vuoksi osittainen soveltaminen (partial application) ei ole mahdollista Erlangissa, eli funktiolle ei voi antaa vain osaa parametreista tuottaen uuden funktion, vaan funktion kaikki parametrit on aina täytettävä kerralla. 2.3. Rinnakkaisuus Vahva tuki rinnakkaisuudelle on keskeinen Erlangin ominaisuus. Rinnakkaisuuden toteuttamiseen käytetään prosesseja. Erlangin prosessit eivät vastaa käyttöjärjestelmän prosesseja, eivätkä edes säikeitä, vaan ne ovat Erlang-virtuaalikoneen toteuttamia vielä kevyempiä suoritusyksiköitä. Prosesseista on haluttu mahdollisimman kevyitä, jotta niiden käynnistäminen ja poistaminen olisi mahdollisimman nopeaa, koska prosesseja täytyy pystyä ajamaan jopa tuhansia kerrallaan. Erlangin prosessit eivät jaa muistia keskenään vaan ne kommunikoivat viestinvälityksellä. Prosessin käynnistäminen palauttaa prosessitunnisteen, jota käyttämällä prosessille voi lähettää viestejä. Prosessilla voi olla myös nimi, jota voi käyttää viestien lähettämiseen. Prosessin suoritettavaksi annettu funktio määrittelee, kuinka se reagoi viesteihin. Kullakin prosessilla on oma postilaatikko, johon viestit saapuvat, ja josta prosessi lukee niitä sitä mukaa kun ehtii. 2.3.1. OTP Koska rinnakkaisuus on Erlangissa niin tärkeää, on sen ympärille kehittynyt useita kirjastojaratkaisuja, jotka helpottavat prosessien hallintaa. Yksi näistä on OTP (Open Telecom Platform). Koska ohjelmissa käytettävät prosessit ovat yleensä rakenteeltaan hyvin samankaltaisia, on nämä samankaltaiset osuudet prosessien luonti ja tuhoaminen, viestien lähetys ja vastaanottaminen toteutettu OTP:n puolella, jolloin toteuttajan vastuulle jää vain ohjelmalle spesifi toiminta. Yksi OTP:n useimmin käytetyistä abstraktioista on palvelin (gen_server), joka tarjoaa perustan prosessin käynnistykselle sekä synkroniselle ja asynkroniselle viestinnälle. Mielenkiintoisena yksityiskohtana gen_server tarjoaa myös mahdollisuuden päivittää prosessin koodia ajonaikaisesti ilman, että prosessin tilaan liittyvää dataa menetetään.
Muita oleellisia abstraktioita ovat gen_fsm, joka tarjoaa äärellisen tilakoneen ominaisuudet ja gen_event, joka tarjoaa laajemman tuen tapahtumien käsittelylle. 2.4. Virheenkäsittely Erlangin yleinen filosofia virheidenkäsittelyssä on Antaa kaatua, koska tieto kaatuneesta prosessista on mahdollista välittää toiselle prosessille, joka käynnistää kaatuneen prosessin uudelleen. Tätä pidetään yksinkertaisempana kuin kaatumisen estämisen yrittämistä kaikin keinoin. Yksittäisen prosessin sisällä Erlang käyttää virheidenkäsittelyyn poikkeuksia. Poikkeuksia on kolmea tyyppiä, virheet (error), poistumiset (exit) ja heitot (throw). Virheet lopettavat prosessin suorituksen. Niitä käytetään, kun törmätään tilanteeseen, jota kutsuva koodi ei osaa käsitellä ja on järkevämpää antaa prosessin kaatua. Poistumiset ja virheet ovat lähes samanlaisia mutta poistumisia käytetään yleensä, kun halutaan välittää tieto prosessin kaatumisesta toisille prosesseille. Poistuminen ei esimerkiksi palauta pinovedosta (stack trace) toisin kuin virhe, koska pinovedoksen lähettäminen toiselle prosessille on yleensä tarpeetonta. Heittoja käytetään ilmoittamaan virheistä, jotka voidaan käsitellä kaatamatta prosessia. Heittojen avulla voidaan palata nopeasti rekursiosta ylimmälle tasolle, jolloin virheen voi käsitellä erillinen funktio ja rekursiivisen funktion ei tarvitse välittää siitä. Poikkeukset voidaan ottaa kiinni try-catch-rakenteella. Kaikki poikkeustyypit voidaan ottaa kiinni, vaikka niiden varsinainen tarkoitus olisikin erilainen, kuten edellisessä kappaleessa on kuvattu. Try-catch-rakenteessa try- ja of-avainsanojen väliin kirjoitetun lausekkeen paluuarvo käsitellään of- ja catch- avainsanojen välissä määritetyllä tavalla ja poikkeukset käsitellään catch- ja end-avainsanojen välissä määritetyllä tavalla. Try-catch-rakenteessa käytetään hahmontunnistusta päättämään millä tavalla minkäkin tyyppinen paluuarvo tai poikkeus käsitellään. Virhetilanteista kommunikoimiseen prosessien välillä Erlang käyttää linkkejä (link), järjestelmäprosesseja (system process) ja monitoreja (monitor). Linkeillä voidaan yhdistää prosesseja niin, että kun yksi prosessi kaatuu myös siihen linkitetyt prosessit kaatuvat. Tämä voi olla hyödyllistä tilanteessa, jossa yhden prosessin häviäminen tekisi laskennan jatkamisen mahdottomaksi. Järjestelmäprosessit vastaanottavat tiedon siitä, että prosessi on kaatunut, jotta prosessi voidaan käynnistää uudelleen. Monitorit muistuttavat linkkejä mutta monitoreilla joitakin lisäominaisuuksia. Monitorit ovat pinottavia, eli yhtä prosessia varten voi olla monta monitoria mutta ne voi poistaa yksi kerrallaan. Monitorit ovat myös yksisuuntaisia, joten monitoroitava prosessi ei tiedä monitorista mitään.
3. Käyttökokemus Tutustuaksemme kieleen toteutimme rinnakkaisuutta hyväksi käyttävän matriisikertolaskimen. Rinnakkaisuutta käytettiin niin että prosessi, joka suorittaa kertolaskufunktion luo uuden prosessin jokaista tulomatriisin riviä varten eli jokainen rivi lasketaan omassa prosessissaan. Luodut prosessit lähettävät tuloksensa takaisin alkuperäiselle prosessille, joka yhdistää ne tulomatriisiksi. Meillä oli jonkin verran kokemusta funktionaalisesta ohjelmoinnista ja rinnakkaisuudesta jo ennakkoon, joten ohjelmaa ei ollut kohtuuttoman vaikeaa toteuttaa. Funktionaalisena kielenä Erlang ei tunnu yhtä sujuvalta kuin vaikkapa Haskell tai F#, koska siinä ei ole operaatioita funktioiden yhdistämistä varten, joten joissain kohdissa oli pakollista turvautua imperatiiviseen tyyliin, kun kahta peräkkäistä funktioita ei voinut yhdistää yhdeksi. Toisaalta Erlangin prosessimalli ja viestinvälitys tuntui luontevalta ja helpolta, koska keskinäisistä riippuvuuksista ei tarvitse jaetun muistin puuttumisen vuoksi välittää. Näin matriisikertolaskuoperaatio oli helppo jakaa prosessien kesken suoritettavaksi. Vaikeinta oli keksiä, kuinka tulokset kerätään takaisin yhteen, mutta siihenkin löytyi kelvollinen ratkaisu.