(toiminnot)

hwechtla-tl: LISP

Kierre.png

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


Aiheeseen liittyvät myös:

LISP on kiva kieli. Se on myös vanha kieli. Se on jännittävä kieli. Mutta ottaen huomioon että LISP oli pitkään funktionaalisen ohjelmoinnin varsinainen äänenkannattaja, olen ollut yllättynyt siitä, kuinka hankalaksi funktionaalinen ohjelmointi siinä on joissain kohdissa tehty. (Pitääpä muuten mainita, että Scheme yrittää olla "oikeaoppinen lisp", siis jollain tavalla Lisp ilman suttua.)

Esimerkkejä oudoista jutuista (Common LISP) (joihin moniin nykyään ymmärrän syyn, kommentit sulkeissa):

Nämä yllä mainitut liittyvät olennaisesti LISP:n homoikonisuuteen. Katso http://sange.fi/~atehwa/scheme-kurssi/homoikonisuus.html

Yksi syy, miksi ML:ssä ja eritoten Haskellissa ei oikein ole tarvetta erilliselle makrosysteemille, on se, että niiden funktiot toimivat aika pitkälle samaan tapaan kuin LISP:n makrot. (Lispissä metalingvistinen abstraktio (makrot) on eriytetty sanan merkitykseen perustuvasta abstraktiosta (funktio).)

kategoria: ohjelmointi


Tuntemattoman kommentti:

Lispin jännittävyyttä lisää sen valtava dynaamisuus. Se ylittää dynaamisuudessa jopa useimmat modernit skriptauskielet. Itse asiassa olen sitä mieltä, että nykyaikaisen kielen tärkeisiin ominaisuuksiin kuuluu kyky kääntää koodia lennossa.

Lisp:n makrokieli on Lisp itse (samaan tapaan kuin Forth:n). Lisp-makro on funktio, joka evaluoidaan käännösaikana. Makron argumentteja ei kuitenkaan evaluoida kuten funktiokutsussa - Lisp-makrot ovat siis laiskasti evaluoituja, toisin kuin Lisp-funktiot. Makron (siis funktion) palauttama arvo (yleensä koodikaava) tulee makrokutsun paikalle.

(Lispissä on myös käsite kääntäjämakro (compiler macro). Niiden käytös tuo muutamia poikkeuksia muihin makroihin liittyviin sääntöihin: 1) kääntäjämakro suoritetaan vasta muiden makrojen jälkeen, juuri ennen varsinaista käännöstä. 2) kääntäjämakro voi palauttaa oman kaavansa muuttumattomana. Kääntäjämakroja käytetään optimoimaan koodia. Tavallisten makrojen käyttöä koodin optimointiin pidetään huonona tyylinä.)

Jos ohjelmoija ei vielä tunne omnipotenttia kaikkivoipaisuutta (tai sekapäistä pahoinvointia) kaiken tämän makrologian jälkeen, lisäksi on käytössä vielä lukumakroja (reader macros t. read macros). Lukumakrot mahdollistavat koodin muodostamisen lukuvaiheessa, kun lähdekoodin tekstiä luetaan merkki kerrallaan. Tämä vastaa lähinnä leksikaalista jäsennystä normaaleissa ohjelmointikielissä. Koska Lisp on Lisp, myös lukumakrot kirjoitetaan tavallisina Lisp-funktioina.

Lukumakrojen eräs hämmästyttävä luova väärinkäyttöesimerkki on Kent M. Pitmanin tekemä ohjelma, joka kääntää Fortran-koodin Lispiksi lukuvaiheessa!

Makroja käytetään Lispissä usein sovelluskohtaisten pikkukielien rakentamiseen (kuten Forth:ssa) sekä kielen laajentamiseen. Hyvänä esimerkkinä on Common Lisp standardissa oleva loop makro:

(loop for x in '(1 2 3) 
      collect x into cc 
      sum x into ss 
      finally (return (+ ss cc)))

Ylläoleva koodi ei näytä Lispiltä koska loop-makro laajentaa kieltä epälispmäiseen suuntaan. Muita hyviä esimerkkejä ovat monet Prolog-toteutukset Lispin sisällä, kyselykielet jne.


KOMMENTTI-1: nimiavaruuksien jakoon on kyllä käytännöllinen syy joka tulee ilmi jos perehtyy makroihin syvällisemmin. Jos tarkkoja ollaan standardi lispissä on ainakin 9 eri 'nimiavaruutta' (funktio, muuttuja, return-form, ...). Ohjelmoija voi käytännössä lisätä niitä äärettömästi. Kun tähän ajatukseen tottuu, alkaa tuntua kauniilta se että muuttuja- ja funktioarvot erotetaan toisistaan eivätkä ne ole erikoistapaus.

atehwa: minun pointtini ei ollut niinkään systeemin sisäinen konsistenssi (uskon ettei Common Lisp ole tässä mitenkään heikoilla) vaan se, että muuttuja-funktioerottelun vuoksi yksinkertaisesti joutuu kirjoittamaan enemmän kuin muuten joutuisi. Ja koska minä olen oppinut funktionaalisen ohjelmoinnin lambdakalkyylista, tuntuu hullulta, että tarvitaan erillistä syntaksia funktion antamiseen arvona.

Tietenkään perinteinen Lisp ei muutenkaan rohkaise korkeamman asteen funktioiden käyttöä niin paljon kuin monet muut funktionaaliset kielet.

Uskon että jakoon on käytännöllinen syy (en ole niin perehtynyt, että arvaisin, mikä se on), mutta abstraktioiden on tarkoitus piilottaa käytännön monimutkaisuuksia ohjelmoijilta, eikö?


(atehwa: tämä oli alun perin kunnianarvoisan osallistujan korjaus väitteeseeni Haskellin funktioista ja LISP:n makroista, mutta koska siinä ei ole mielestäni mitään, mikä kiistäisi kyseisen väitteen, irrotan sen yhteydestään.)

Tavallisisssa ohjelmointikielissä menetellään seuraavasti:

  1. kirjoitetaan koodia (tekstitiedosto),
  2. (jos kysessä C-kieli ajetaan C:n makrokieli tässä välissä.)
  3. käännetään koodi (tuloksena suoritettava ohjelma),
  4. ajetaan koodi (suoritusaika).

Common Lispissä:

  1. kirjoitetaan koodi (tekstiä).
  2. suoritetaan luku-makrot. Tämän vaiheen jälkeeen ei koodia ole enää tekstuaalisessa muodossa vaan tietorakenteina: listat, symbolit, taulukot ym. Normaleissa ohjelmointikielissä kysessä olisi kääntäjän sisäinen jäsennyspuu, Lispissä koodi on dataa eli listoja yms.
  3. suoritetaan makrot. (jäsennyspuun muokkausta)
  4. kääntäjä-makrot. (lisää muokkausta, optimointitarkoituksessa)
  5. käännos konekoodiksi
  6. ladataan koodi (mielivaltaisia lisp funktioita voidaan suorittaa myös lataus-aikana)
  7. ajetaan koodi.

Muissa ohjelmointikielissä kirjoitetään koodia, annetaan se kääntäjälle ja saadaan suorituskelpoinen ohjelma, Lispissä on mahdollista muokata kääntäjän jäsennyspuuta makrojen avulla ennen varsinaistä käännöstä konekoodiksi. Nyt ehkä alkaa selvitä miksi jotkut kutsuvat Lispiä ohjelmoitavaksi ohjelmointikieleksi.

atehwa: Minunkin olisi varmaan syytä selventään omaa näkemystäni makroista. Makrot ovat funktioita koodista koodiin; syöte- ja tuloskoodin muoto voi vaihdella paljonkin, esim. cpp (C-ohjelmointikielen makroprosessori) kohtelee koodia aina merkkivirtoina, Lisp:ssa on erikseen makroja merkkivirroista syntaksipuihin ja syntaksipuista syntaksipuihin (eikä (kai) ollenkaan merkkivirroista merkkivirtoihin).

Mitä voimakkaampia funktionaalisen kielen funktiot ovat, sitä enemmän niihin saa siirretyksi makrojen toiminnallisuutta. (En väitä, että tämä olisi pyrkimisen arvoista, mutta haluan selventää väitettäni siitä, että esim. Haskellissa makroja ei usein tarvita siinä, missä LISP:ssa niitä tarvitaan.) Esimerkiksi, jos kieli (tai kyseiset funktiot) on referentially transparent, voidaan abstrahoida näkymättömiin se, missä vaiheessa funktio/makro laajennetaan; ja jos kieli on laiskasti evaluoitu, funktiot ovat "voimakkaampia" (pystyvät estämään argumenttiensa evaluaation). Edelleen, jos korkeamman asteen funktioihin ei tarvitse erityistä syntaksia, uudet kontrollirakenteet voi mallintaa läpinäkyvästi funktioina. Ja idempotenssi aiheuttaa sen, että kaksinkertaisesta evaluaatiosta ei ole mitään hyötyä (tämä tarkoittaa käytännössä, että makrojen laajenteiden ei tarvitse olla koodirepresentaatioita kuten listoja vaan ne voivat olla suoraan koodia).

Ainoat, mihin tällaiset funktiot eivät käy, ovat leksikaalisten sulkeumien rikkominen ja lukumakrot. Näitä varten uudemmissa funktionaalisissa kielissä on yleensä laajennettavia parsereita.


LISP on siitä merkittävä kieli, että sillä pystyy kirjoittamaan hyvin korkean tason koodia samalla, kun sen toimintaan pystyy puuttumaan hyvin matalalla tasolla. Mutta valitettavasti minun kokemukseni on se, ettei LISP:a pysty kunnolla käyttämään ilman, että myös tajuaa, miten se toimii matalalla tasolla. Schemeä voi hyvin pystyäkin.


CL:n ystävä: Yllä vertaillaan Common Lisp -kieltä funktionaalisiin kieliin. CL ei ole varsinaisesti funktionaalinen kieli, vaan ennenkaikkea käytännöllisyyteen tähtäävä moniparadigmakieli (hyvänä esimerkkinä: standardi ei velvoita tail call eliminationin tukea implementaatioilta; joissakin se pitää erikseen pyytää laittamalla (declare (optimize speed)) funktion alkuun). Enemmän funktionaaliseen ohjelmointiin painottuu toinen nykyään dominantti Lisp-murre Scheme, jossa mm. pakollinen tail call elimination, vain yksi nimiavaruus, sekä mielenkiintoisena ominaisuutena käyttäjälle näkyvät jatkeet (continuations). Scheme on minimalistinen ja monet käytännön ohjelmoinnissa tarvittavat toiminnot on itse kirjoitettava tai käytettävä Scheme-implementaatioiden epästandardeja toteutuksia tai puolistandardeja laajennusehdotuksia, SRFI:a.

CL:n voisi jakaa konseptuaalisesti kahteen alakieleen: puurakenteiden käsittelykieli sekä moderni ohjelmistorakenteiden kuvauskieli (olioluokat, signaalit, funktiot, numeeriset operaatiot, tietorakenteet, ym.) Jälkimmäisestä huomionarvoisia esim. ilmaisuvoimainen CLOS-oliojärjestelmä sekä sen puolistandardi metaolioprotokolla joka mahdollistaa introspektion sekä CLOSin semantiikan muokkaamisen; sekä signaalijärjestelmä joka tukee ei-poikkeuksellisia signaaleita sekä introspektiota järjestelmän tilan suhteen signaalinkäsittelijäfunktioissa.

Koko em. järjestelmää ohjataan siis puurakenteilla kuvattujen ohjelmien avulla, joiden manipulointiin (metaohjelmointi, dynaaminen ohjelmointi) makrot ja listakeskeisyys ovat luonnollisia työkaluja. Tämän mielekkyydestä voi olla monta mieltä. CL:n päälle olisi mahdollista rakentaa miten ei-Lispmäisiä syntakseja sekä semanttisia malleja tahansa (muistaakseni esim. joku Haskell-järjestelmä oli rakennettu CL:n päälle) mutta sitä ei juuri harrasteta Lisp-koodarien keskuudessa (aluekohtaisia makroja toki harrastetaan) sillä perus-CL miellyttää useimpia Lisp-käyttäjiä. Se on totta, että kielen oppimiskynnys on korkeahko ja kieleen täytyy todella perehtyä pystyäkseen hyödyntämään sen ilmaisuvoimaisia ominaisuuksia.

atehwa: Yllä on monia erittäin hyviä huomioita. Lisp on kokonaan oma ajatusmaailmansa. CL sen varianttina (tai InterLisp sen puoleen) ei kannata erityisemmin rekursiivisia määrittelyitä, mikä on perinteistä funktionaalisessa ohjelmoinnissa. Kaikki Lisp-kielet ovat järkyttävän pitkälle muokattavissa, sallien esimerkiksi toteuttaa suhteellisen helposti vaihtoehtoisia syötesyntakseja ohjelmille, mutta jotakuinkin kaikki, jotka tämän osaisivat tehdä, pitävät myös Lisp-kielten luontaista syötesyntaksia (s-lausekkeet) parhaana mahdollisena. :)


kommentoi (viimeksi muutettu 15.02.2011 21:43)