(toiminnot)

hwechtla-tl: Yhdistetyt merkkien kaltaiset tietotyypit

Kierre.png

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


Nykyään ohjelmoijat pitävät jotenkin itsestään selvänä seuraavaa jakoa: on olemassa lukuja, sitten on olemassa merkkejä, ja merkeistä koostuu merkkijonoja. Luvuilla voi osoittaa merkkijonoista kohtia, mutta se onkin ainoa, miten ne liittyvät merkkijonoihin.

Lukiessani taas pitkästä aikaa Logon manuskoja (ja muuta Logoon liittyvää kirjallisuutta) muistin, miten siunatun yksinkertainen Logon tyyppihierarkia oli. Logossa oli oikeastaan vain kahden tyyppisiä arvoja: sanoja ja lauseita. Sanat olivat yksinkertaisia, atomisia ilmauksia, sellaisia kuin kahvi, Heipä.hei, 3 tai laatutarkastusvaliokunta. Lauseet (joita joskus kutsuttiin listoiksi) olivat sanojen ja alilauseiden järjestettyjä joukkoja, esimerkiksi [Minun nimeni on Kalle Pikkarainen] ja [Luulen, että [huomenna sataa kaatamalla]]. Nämä tietotyypit oli kehitetty vastaamaan alkeellisesti ihmisten kokemusmaailman asioita, eikä teknisiä toteutukseen liittyviä asioita.

Yksi, missä tietotyyppien intuitiivinen voima hyödynnettiin erinomaisesti, oli Logo-kielen itsensä rakenne. Jokainen komento oli lause, esimerkiksi [forward 100] tai [penup home pendown]. (Uloimmat hakasulkeet jätettiin pois, joten komentolauseet olivat lapsille suhteellisen helppoja kirjoittaa. Samasta syystä, kirjoittamisen helppoudesta, useimmille käskysanoille oli kaksikirjaimiset lyhenteet, esim [pu home pd].) Käskysanat, kuten forward, olivat sanoja (tietenkin). Niiden argumentit olivat sanoja, ilmauksia tai lauseita sen mukaan, millaista tietoa käskyn tarvitsivat tehtävänsä toteuttamiseen. Esimerkiksi lauseessa [forward 100] luku 100 on forward-käskyn objekti, sana joka kuvaa, kuinka paljon mennään eteenpäin. Lauseessa [forward random 100] ilmaus random 100 edustaa satunnaista lukua yhdestä sataan. Ja lauseessa [repeat 3 [print [Olet kiva]]] on alilause [print [Olet kiva]], joka edustaa toimintaa, jota toistetaan 3 kertaa -- sekä alialilause [Olet kiva], joka kertoo, mitä tulostetaan. Jännittävää kyllä, tämä alialilause on eri kieltä (eli Suomea) kuin sitä ympäröivät lauseet.

Lauseet, toisin kuin Lispin listat, olivat symmetrisiä: niitä pystyi käsittelemään lisäämällä tai poistamalla osia (sanoja tai alilauseita) alussa tai lopussa, yhdistämään kaksi lausetta yhdeksi (tämän tekevän ilmaussanan nimi muuten oli sentence tai se), ja tarkastelemaan niitä osa kerrallaan. Ne olivat yleinen tietotyyppi kaiken yhdistelmätiedon varastoimiseen, voisin esimerkiksi varastoida kaiken olennaisen tiedon ystävästäni lauseeseen [Wolmar [vaatteiden-väri vihreä] [seksuaalinen-orientaatio homo] [yleisin-puheenaihe formulat sähkökaapelointi]].

Sanat olivat mielenkiintoisempia suuremman rajoitettuutensa vuoksi. Niillä ei tavallaan ollut rakennetta, mutta onhan pakko olla jokin keino hajottaa sanoja esim. voidakseen testata, onko niiden ensimmäinen kirjain iso. Tätä varten sanoista pystyi ottamaan kirjaimen kerrallaan alusta tai lopusta. (Logossa muuten sanan ensimmäinen kirjain ja lauseen ensimmäinen osa (sana tai alilause) otettiin molemmat polymorfisella ilmaussanalla first, mikä joskus johti mielenkiintoisiin bugeihin.) Kirjaimet eivät kuitenkaan olleet erillinen tietotyyppi, vaan sanoja: kun esimerkiksi sanomme, että "Tuoli alkaa T:llä", onhan tuolloin "T" oma sanansa, eikö totta? Sanoja pystyi myös yhdistämään kahdesta sanasta: esim. ilmaus word "kahvi "lusikka tarkoitti samaa kuin "kahvilusikka. Mutta koska sanojen sisäiseen rakenteeseen ei pysty vaikuttamaan, ilmaus first word "kahvi "lusikka tarkoittaa samaa kuin "k (kahvilusikan ensimmäinen kirjain), kun taas first sentence "kahvi "lusikka tarkoittaa samaa kuin "kahvi (lauseen [kahvi lusikka] ensimmäinen sana).

(Mielenkiintoisena sivujuonteena todettakoon, että Tcl:ssa yksiosainen lista ja atomi on sama asia, kun taas alilistalle on oma tietotyyppinsä. Tästä pystyisi ehkä kehittämään jonkinlaisen järkevän järjestelmän; Tcl ei sitä tee.)

Mutta vanhoja Lisp-manuskoja lukemalla sain tietää, että tämä sama ajatus oli alun perin Lispinkin pohjana. Alkuperäisessä lispissä oli vain kahdenlaisia tyyppejä, atomeja ja pareja. Atomi vastaa lähes täydelleen Logon sanaa (mielivaltainen, yksilöllinen nimi vailla sisäistä rakennetta), kun taas pari oli tavallaan kaksikohtainen lause, jonka niin ensimmäinen kuin jälkemmäinenkin kohta saattoi puolestaan olla atomi tai toinen pari. Listoja muodostettiin näistä pareista siten, että lista määriteltiin olevaksi joko (1) tyhjä lista, jota esitettiin atomilla NIL tai (2) ei-tyhjä lista, jota esitettiin parilla, jonka ensimmäinen kohta oli listan ensimmäinen osa (atomi tai alipari) ja toinen kohta oli toinen lista, jossa oli loput listan sisällöstä. (Tämä parien ja listojen määritelmä on Lisp-henkisissä kielissä säilynyt, kun taas atomin käsite on hajonnut ajan tuulessa.)

Samoin kuin Logossa, Lispissäkin numerot ja kirjaimet olivat vain atomin (sanan, symbolin) alatyyppi, ja samoin kuin Logossa, Lispissäkin tarvittiin jokin keino hajottaa atomeita (sanoja). (Käsittelymme on muuten siinä mielessä anakroninen, että Logo on tietenkin suunniteltu Lispin pohjalta eikä päin vastoin.) Lisp 1.5 lähestyi atomiytimen rakennetta eri tavalla kuin Logo: koko atomien sisäisen rakenteen manipulointi perustui yhdelle fissio-operaatiolle (unpack) ja yhdelle fuusio-operaatiolle (pack). Esimerkiksi (pack '(yksi kaksi 3)) tuotti atomin 'yksikaksi3 ja (unpack 'yksikaksi3) tuotti listan '(y k s i k a k s i 3). Jopa sellaiset kummallisuudet kuin (unpack 655) ja (pack '(foo (bar baz))) toimivat. Nämä rutiinit olivat edelleen olemassa INTERLISP-ympäristössä, josta voisin muuten mainita, että Lisp-koneiden INTERLISP-D oli melko varmasti hienoin ohjelmointiympäristö (vaikkei välttämättä kieli), jonka maailma on tähän päivään mennessä nähnyt.

Jossain vaiheessa kuitenkin tästä omalaatuisesta ja yksinkertaisesta järjestelmästä luovuttiin, ja Lispiin omaksuttiin yhä enemmän muista ohjelmointikielistä tuttuja tyyppejä, jotka tavallaan olivat jo tuettuina Lisp:ssä eri muodossa. Merkkijonot olivat todennäköisesti ensimmäinen. Niiden hyödyllisyys perustuu siihen, että on olemassa monia tilanteita, joissa on paljon rakenteetonta dataa; rakenteettomuus viittaa siihen, että se olisi parempi mallittaa atomeilla, mutta atomeiden hankalahko ydinreaktiokäsittely aiheuttaa sen, että ne eivät ole käteviä suurille tietomäärille. Tarvittiin "helposti muokattavaa" atomia, merkkijonoa. Merkkijonot oli mahdollista toteuttaa atomeja helpommin erityisesti siksi, että niiden ei tarvinnut olla uniikkeja: kaksi samansisältöistä merkkijonoa sai olla eri merkkijono (kaksi samannäköistä atomia tai sanaa olivat aina sama atomi/sana). Kun merkkijonot oli lisätty kieleen, merkkien lisääminen tuli luonnolliseksi. Esimerkiksi nykyisessä Schemessä, jos haluan katsoa, onko symbolin ensimmäinen kirjain iso, kirjoitan (char-upper-case? (string-ref (symbol->string symb) 0)). Ja kun merkit ja merkkijonot ovat olemassa eivätkä luvut ole enää atomeja, monille alkaa olla vaikeaa enää ymmärtää, miksi symbolit ovat olemassa. Ennen kuin olin tutustunut tähän asiaan, näin jossain FAQ-vastauksia, joissa kerrottiin, miksi (pack) ja (unpack) olivat kiellettyjä ja tuomittavia kapistuksia.

Olisi hienoa, jos saataisiin palautetuksi atomien kaunis yksinkertaisuus. (pack) ja (unpack) bannattiin ilmeisesti lähinnä siksi, että niiden käyttäminen edellytti usein pitkiä listoja ja kulutti siksi paljon muistia. Mutta jos haluan tehdä nykyään symbolin merkeistä (kuten esim. parserissa tarvitsee), minulla on kovin vähän muita vaihtoehtoja kuin akkumuloida merkkejä listaan ja sitten tehdä (string->symbol (list->string (reverse lista))). (unpack):n voi mielestäni heittää helvettiin, mutta sen sijaan meillä voisi kyllä olla symbol-ref, jonka saakin Schemessä erittäin helposti toteutetuksi.

Ennen kaikkea olisi hienoa, jos merkki-tietotyypistä päästäisiin eroon. Kun merkkejä oli vain 127 erilaista, oli ehkä vielä jotain järkeä (tehokkuuden kannalta) eriyttää merkki-tietotyyppi kokonaan muista symboleista. Mutta nykyään, Unicoden aikana, merkki on taas tulossa siksi, mitä se on koko historiallisen ajan ollut: symboli, atomi, yksittäinen merkitysyksikkö, joka ei hajoa osasiin. (Itse asiassa merkin voisi määrittää symboliksi, jonka ainoa osa on merkki itse.)

On silti mahdollista, että merkkijonoja tarvitaan suurten tietomäärien käsittelyyn. Tämä ei kuitenkaan estä kohtelemasta merkkejä symboleina. Merkkijonojen tehokkuus voi edelleen perustua sille, että ne voivat sisältää vain merkkejä eikä niiden tarvitse olla uniikkeja. Jos ihmiset käyttäisivät merkkijonoja vain siihen, mihin niitä todella tarvitaan (suurten, rakenteettomien tietomäärien käsittelyyn), ja käsittelisivät lyhyitä ilmauksia edelleen lauseina (atomien listoina), merkkijonototeutuksetkin voisivat paremmin erikoistua nimenomaan suurten tietomäärien varastointiin, indeksointiin jne. Tämän hetken ohjelmointikielissä merkkijonot soveltuvat parhaiten yleensä nimenomaan erittäin pienten tietomäärien käsittelyyn ja suuriin tietomääriin tarvitaan erikoisratkaisuja.


Olen tullut tämän suhteen vieläkin jyrkemmälle kannalle ja nykyään olen enimmäkseni sitä mieltä, että merkkijonot voisi korvata kokonaan lauseilla ja ns. tavuvektoreilla. Tavuvektorit ovat suunnilleen Unix-tiedoston (tai TCP-yhteyden) sisältöä vastaava tietotyyppi: järjestetty, tietynmittainen jono lukuja välillä 0--255. Nämä luvut voi tulkita merkeiksi halutessaan, ja usein ne tulkitaankin. Toivoisin, että ohjelmointikielessä merkkijärjestelmä olisi atomien ja tavuvektorien välinen rajapinta.


Olen viime aikoina kehitellyt tietotyyppiä, joka olisi jonkinlainen yhdistelmä Logon "lausetta" ja "sanaa". Idea on se, että jokaista tietorakennetta voisi käsitellä yhtä lailla lauseena tai sanana: lauseena se koostuu osista (jotka voivat olla sanoja tai toisia lauseita), sanana se koostuu merkeistä. On vaikeaa suunnitella ohjelmointirajapintaa, joka sallisi yhdistää nämä kaksi ilman, että joudutaan luopumaan datan rakenteellisuudesta.

Idea on tämä: lauseina dataa voi destrukturoida ottamalla sanan kerrallaan alusta tai lopusta. Merkkeinä sitä voi destrukturoida vain indeksoimalla. Molemmissa tapauksissa uusia arvoja voi luoda yhdistämällä kaksi vanhaa. Jos kaksi lausetta yhdistetään sanoina, vain niiden toisiaan lähimpänä olevat sanat yhdistyvät. Lisäksi minkä tahansa ilmauksen voi lisätä lauseen loppuun tai alkuun, niin että on taattua, että se pidentää lausetta vain yhdellä. Ainoa ongelma, johon en ole keksinyt nättiä ratkaisua, on se, jos alilause pitäisi yhdistää sanana. En voi tuhota koko alilauseen rakennetta vain saadakseni sen yhdistettäväksi sanaksi. Niinpä olen päättänyt, että silloin se vain yhdistetään, ikään kuin kyse olisi lauseyhdistämisestä.

Olen väsännyt Schemellä toteutuksen, joka käyttää sana- ja merkkimäärän suhteen indeksoituja splay-puita, jotta oikeasti sekä alku- että loppurekursiot olisivat tehokkaita. Tämä on ollut aika hauskakin projekti...

kategoria: ohjelmointi


kommentoi (viimeksi muutettu 15.04.2013 10:50)