Tässä käsitelty aihe on vaikea ja teoreettinen. Siihen ei välttämättä
kannata tutustua, ellei ole pakko. Pakko
saattaisi olla esim. siinä
vaiheessa, kun symbolien ja listojen erilaiset syntaksit alkavat
kummastuttaa. Esimerkiksi listan voi kirjoittaa molemmilla seuraavista
tavoista:
'(1 3 #\r) (list 1 3 #\r)
Tämän dokumentin on tarkoitus selittää syy tähän ja muutamaan muuhunkin asiaan.
Itse asiassa homoikonisuus tarkoittaa useampiakin asioita, mutta Lisp-perheen kielten yhteydessä sillä on vakiintunut merkitys. Se tarkoittaa, että ohjelmilla (lausekkeilla, funktioilla jne.) on (yksiselitteinen) esitystapa tietorakenteina. Toisaalta tietorakenteilla on (yksiselitteinen) esitystapa tekstinä eli merkkijonoina.
Mitä tämä tarkoittaa? Tarkastellaan ilmausta (+ 3 5). Kun se on
ohjelmatiedostossa tai käyttäjä kirjoittaa sen, se on merkkijono (+ 3
5)
ja siinä on seitsemän merkkiä (kaksi välilyöntiä ja viisi muuta
merkkiä). Mutta se sattuu olemaan myös kolmielementtisen listan
esitysmuoto tekstinä. Schemen sisäänrakennettu funktio (read) osaa
lukea näitä ulkoisia esitysmuotoja
ja palauttaa niitä vastaavan
tietorakenteen; tässä tapauksessa listan, jossa on kolme elementtiä
(symboli +, luku 3 ja luku 5).
Se, onko ilmaus (+ 3 5) tietorakenne vai pätkä ohjelmaa, riippuu siitä, mitä sillä tehdään. Jos haluamme kohdella sitä tietorakenteena, voimme tehdä sille listaoperaatioita — esimerkiksi car palauttaa siitä symbolin +. Jos taas haluamme kohdella sitä ohjelmana, annamme sen tyypillisesti funktiolle eval, joka suorittaa sen ohjelmana ja palauttaa, mitä suorituksesta on tuloksena — esimerkkitapauksessamme luku 8.
Tarkastellaan jotain vielä yksinkertaisempaa ilmausta, esimerkiksi symbolia foo. Sekin voidaan nähdä tietorakenteena tai ohjelmana. Tietorakenteena sitä voi esim. verrata muihin symboleihin eqv?-predikaatilla. Mutta, jos se suoritetaan ohjelmana, se onkin muuttuja, ja eval palauttaa tällöin muuttujan foo arvon.
Homoikonisuus aiheuttaa jonkin verran väärinkäsityksiä. Yksi ongelma on
se, että tietyt asiat tuottavat saman tuloksen suoritettuina kuin mitä
ne ovat tietorakenteina. Esimerkiksi luku 3 on tietorakenne, mutta
toisaalta sen suorittaminen ohjelmana tuottaa myös luvun 3. Samoin
merkkijono väsyttää
on toisaalta tietorakenne, mutta toisaalta
ohjelma, joka eval:lle annettuna tuottaa tismalleen saman tuloksen,
nimittäin merkkijonon väsyttää
. Tällaisista rakenteista sanotaan,
että ne evaluoituvat itsekseen
, ja niihin kuuluvat lukujen ja
merkkijonojen lisäksi totuusarvot ja merkit.
Sen sijaan esimerkkejä rakenteista, jotka eivät evaluoidu itsekseen ovat juuri listat (jotka tulkitaan funktiokutsuina), symbolit (jotka tulkitaan muuttujiin viittaamisena) sekä hiukan yllättäen taulukot (joille ei ole mitään evaluointitulkintaa).
Operaatio quote, jolla on lyhennemerkintänä heittomerkki, estää argumenttinsa evaluoimisen. Esimerkiksi:
(define x y) (define x 'y)
Näistä lausekkeista ensimmäinen antaa muuttujan x arvoksi saman kuin y:n arvo. Toinen sen sijaan antaa muuttujan x arvoksi symbolin y. Tämä johtuu siitä, että quote esti ilmauksen y tulkinnan muuttujana ja jätti sen sen sijaan symboliksi. Samaan tapaan:
(define x (+ 3 5)) (define x '(+ 3 5))
Ensimmäinen lauseke sijoittaa x-muuttujaan luvun 8, sillä se on ilmauksen (+ 3 5) evaluoinnin tulos. Sen sijaan toisessa quote estää listan evaluoinnin, jolloin x-muuttujaan päätyykin kolmielementtinen lista (jonka elementit ovat symboli + sekä luvut 3 ja 5).
Sen sijaan esimerkiksi näillä ei ole mitään eroa:
(define x "foo") (define x '"foo")
Ensimmäisessä tapauksessa foo
evaluoidaan, ja tuloksena on
foo
, joka sitten sijoitetaan x-muuttujaan. Toisessa tapauksessa
foo
:a ei evaluoida, vaan quote palauttaa sen sellaisenaan, ja
se sijoitetaan x-muuttujaan. Vaikutus on tismalleen sama.
Toinen, mikä usein aiheuttaa käsittämisvaikeuksia ihmisille, on se, kuinka ohjelmia suoritetaan eli kuinka eval toimii. Esimerkiksi ilmauksen (* 2 x) suorittaakseen eval tekee monta asiaa:
Jos eval kohtaa listan sisällä toisen listan, tämä alalista evaluoidaan ensin; esimerkiksi ilmauksen (car (cdr mylist)) suoritus etenee näin:
Homoikonisuudesta seuraa suoraan se, että listat ja symbolit ovat erityisiä. Niitä ei voi kirjoittaa sellaisinaan lähdekoodiin, kuten lukuja, totuusarvoja yms., vaan ne pitää suojata quote:lla (tai sen lyhennemerkinnällä heittomerkillä). quote suojaa evaluoinnilta koko argumenttinsa. Esimerkiksi ilmauksesta (quote (x y z)) on suoritettaessa tuloksena lista, jossa on kolme symbolia (x, y ja z); ei lista, jossa on muuttujien x, y ja z arvot. Tämän vuoksi, jos halutaan perustaa lista, jossa jokin elementti on laskettu tai tulee muuttujasta, tarvitaan operaatiota list (tai cons). Seuraavat ilmaukset tuottavat suoritettuina saman tuloksen:
'("foo" bar 3) (list "foo" 'bar 3) (cons "foo" (cons 'bar (cons 3 '())))
Jos listoja on paljon sisäkkäin, tilanne voi tulla varsin monimutkaiseksi. Seuraavat ilmaukset tuottavat myös keskenään saman tuloksen:
'(vp (n "poika") (v "juoksee")) (list 'vp (list 'n "poika") (list 'v "juoksee")) (cons 'vp (cons (cons 'n (cons "poika" '())) (cons (cons 'v (cons "juoksee" '())) '()))
Myös taulukot on suojattava evaluaatiolta quote:lla. Niiden evaluoimisella ei ole määritettyä arvoa, vaan se on virhe:
(define x #(1 4 5 6)) ; VIRHE (define x '#(1 4 5 6)) ; sijoittaa taulukon muuttujaan x
Homoikonisuudella on myös mielenkiintoisia käytännön seurauksia. Yksi on se, että ohjelmia voi tarkastella tietorakenteina:
(if (eqv? '+ (car ilmaus)) (error "En suostu tekemään yhteenlaskua") (eval ilmaus))
Yksi ensimmäisiä ohjelmia, joka Lisp:lla aikoinaan kirjoitettiin, oli
Lisp-kielen toteutus. Se on vain muutamia kymmeniä rivejä pitkä.
Lisäksi on verrattain helppoa kirjoittaa yksinkertaisia kieliä, joissa
on samanlainen (read) (eli merkit tulkitaan samalla tavalla
tietorakenteeksi) mutta erilainen (eval) (eli tietorakennetta
suoritetaan eri tavalla). Esimerkiksi tekoälyohjelmissa on joskus
sisäinen kyselykieli
, joka saattaa näyttää esim. tältä:
; hae yli 50-vuotiaat ihmiset, joiden äiti asuu Helsingissä (and (ikä x ikä) (> 50 ikä) (äippä x y) (asuu-kaupungissa y "Helsinki")) ; voitaisiin käyttää esim. näin (huom! fetch-tuples on siis se, jossa ; koko kyselykieli on varsinaisesti toteutettu) (fetch-tuples '(and (ikä x ikä) (> 50 ikä))) ; tulos voi olla esim. lista erilaisista x:n ja ikä:n arvoista...
Schemessä on myös operaatio quasiquote, jonka voi lyhentää käänteisheittomerkillä (`). Se toimii muuten niin kuin quote, paitsi että sekaan voi livauttaa pilkulla merkittynä kohtia, jotka sittenkin evaluoidaan. Esimerkiksi nämä ilmaukset tuottavat saman suoritettuina:
'(x y 4 (+ 2 1)) (list 'x 'y (+ 2 2) '(+ 2 1)) w_bq (x y ,(+ 2 2) (+ 2 1))
quasiquote on ehdottoman hyödyllinen, jos listan tai taulukon osana
on jotain, minkä arvo ei ole vakio. Esimerkiksi seuraava funktio
tuottaa lausetta punainen x laulaa villisti
vastaavan syntaksipuun
(jossa x on vaihtuva osio):
(define (make-syntax-example x) w_bq (vp (np (adj "punainen") (n ,x)) (vp (adv "villisti") (v "laulaa"))))
Tällaisen tekeminen list-operaatiolla on lähinnä itsensä turhaa kiduttamista… Toinen hyvä esimerkki voisi olla henkilötietorakenteen muodostava funktio:
(define (make-person name age city) w_bq (:person #(,name ,age ,city)))