(toiminnot)

hwechtla-tl: Nimien uudelleenmäärittely clojuressa

Kierre.png

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


(nettipäiväkirja 25.08.2023) Tajusin vihdoin ja viimein, miten Clojuressa nimien uudelleenmäärittely toimii! Tää on tosi tärkeää, koska tästä on kiinni, pystyykö Clojuressa saamaan lähdekoodissa muutetut määrittelyt heti käyttöön, vai pitääkö jokin osa sovelluksesta (tai koko runtime) käynnistää uudelleen. Ja Clojuren käynnistäminen uudelleen on _tosi_ hidasta.

Clojure kohtelee eri tavalla nimiä, kun niihin viitataan funktioiden sisällä ja ulkopuolella. Funktioiden sisällä ne viittaavat epäsuorasti "Var"-nimisten olioiden kautta arvoihin. Funktioiden ulkopuolella katsotaan välittömästi, mihin Var viittaa, haetaan sen sisältö, ja käytetään sitä. Tämä ehkä selventää asiaa:

;; perustetaan yksi nimiavaruus, jonka arvoa tullaan muuttamaan, ja
;; toinen, joka käyttää sitä
user=> (ns kokeilu)
nil
kokeilu=> (def x "vanha arvo")
#'kokeilu/x
kokeilu=> (ns toinen)
nil
toinen=> (require '[kokeilu :refer [x]])
nil
toinen=> x
"vanha arvo"

;; viitataan x:iin funktion ulko- ja sisäpuolelta.  Tässä vaiheessa
;; tulos on varsin samanlainen.
toinen=> (def arvo (str x " kuten näkyy"))
#'toinen/arvo
toinen=> (defn funktio [] (str x " kuten ei näy"))
#'toinen/funktio
toinen=> arvo
"vanha arvo kuten näkyy"
toinen=> (funktio)
"vanha arvo kuten ei näy"

;; muutetaan toisen nimiavaruuden sisältöä.
toinen=> (in-ns 'kokeilu)
#object[clojure.lang.Namespace 0x29b98294 "kokeilu"]
kokeilu=> (def x "uusi arvo")
#'kokeilu/x

;; nyt funktiossa oleva viittaus näkee muutokset, mutta arvo pysyy
;; samana arvona.
kokeilu=> (in-ns 'toinen)
#object[clojure.lang.Namespace 0xcb8108a "toinen"]
toinen=> arvo
"vanha arvo kuten näkyy"
toinen=> (funktio)
"uusi arvo kuten ei näy"

;; myös toinen-nimiavaruudessa x viittaa uuteen arvoon, jännää!
toinen=> x
"uusi arvo"

Mitä tässä tapahtuu? Kun clojure kääntää funktio:n määrittelyä, se ei oikeasti hae x:n arvoa, vaan kääntää funktioon mukaan koodin, joka hakee x:n arvon vasta kun funktiota kutsutaan. Clojure siis tallettaa funktioon viittauksen siihen var-olioon, jossa "vanha arvo" ja sittemmin "uusi arvo" on tallessa. (def) taas löytää saman var-olion sen perusteella, että kokeilu-nimiavaruus sisältää viittauksen x-symbolista var:iin.

Se, miksi "x" toimii yksinään myös toinen-nimiavaruudessa, on se, että se on myös viittaus samaan var-olioon eli sama päivitettävä var löytyy molempien nimiavaruuksien kautta. Mutta kun kutsutaan (str), ei x-ilmausta voi jättää viittaukseksi vaan sieltä on pakko hakea varsinainen arvo, jotta (str) pystyy laskemaan lopputuloksen.

Tämä on siinä mielessä harmillista, että monet asiat, jotka olisivat tosi tärkeitä pystyä määrittelemään uudelleen, eivät ole viittauksia. Esimerkiksi, jos webbiserveriin on tehty handler-funktio (defroutes):lla, sen arvo lasketaan tyypillisesti heti, ei viivästetysti.

toinen=> (require '[compojure.core :refer :all])
nil
toinen=> (def hyvä-route (GET "/" [] {:status 200}))
#'toinen/hyvä-route
toinen=> (def huono-route (GET "/huono" [] {:status 500}))
#'toinen/huono-route
toinen=> (defroutes foo hyvä-route huono-route)
#'toinen/foo
toinen=> (foo {:uri "/" :request-method :get})
{:status 200, :headers {}, :body ""}
toinen=> (foo {:uri "/huono" :request-method :get})
{:status 500, :headers {}, :body ""}

;; jos muutan yhden routen määrittelyä, se ei näy
toinen=> (def huono-route (GET "/huono" [] {:status 500 :body "ounou"}))
#'toinen/huono-route
toinen=> (foo {:uri "/huono" :request-method :get})
{:status 500, :headers {}, :body ""}

;; määrittelyn laskeminen uudelleen tietysti auttaa (koska käyttää
;; huono-routen uutta arvoa)
toinen=> (defroutes foo hyvä-route huono-route)
#'toinen/foo
toinen=> (foo {:uri "/huono" :request-method :get})
{:status 500, :headers {}, :body "ounou"}

... Mutta ihan yksinkertainen eetalavennus (eli käytännössä wrappaus funktioon) korjaa tämän:

toinen=> (def foo #((routes hyvä-route huono-route) %))
#'toinen/foo
toinen=> (foo {:uri "/huono" :request-method :get})
{:status 500, :headers {}, :body "ounou"}
toinen=> (def huono-route (GET "/huono" [] {:status 500 :body "nyt taas hyvä"})) 
#'toinen/huono-route

;; nyt foo saa uuden määrittelyn koska nimetön #()-funktio sisältää
;; viittauksen huono-routen var-olioon joka on juuri päivitetty
toinen=> (foo {:uri "/huono" :request-method :get})
{:status 500, :headers {}, :body "nyt taas hyvä"}

Niinpä voisi vaikuttaa siltä, että kaikesta vaan pitää tehdä funktioita väen vängällä viivästääkseen muuttujaviittausten laskemista, mutta clojuressa on toinenkin ratkaisu, joka on vielä vaikeampi ymmärtää.

Var-oliot nimittäin toimivat kuin funktiot. Ne delegoivat funktiokutsut sille funktiolle, jonka sisältävät.

toinen=> (hyvä-route {:uri "/" :request-method :get})
{:status 200, :headers {}, :body ""}
toinen=> (#'hyvä-route {:uri "/" :request-method :get})
{:status 200, :headers {}, :body ""}

;; tämä tosiaan toimii vain vareille, jotka sisältävät funktioita:
toinen=> (def y 3)
#'toinen/y
toinen=> (+ #'y 3)
Execution error (ClassCastException) at toinen/eval15232.
class clojure.lang.Var cannot be cast to class java.lang.Number

Var, joka sisältää funktion, käyttäytyy siis kuin funktio, mutta sen kutsuma funktio myös päivittyy uuteen arvoon, mikäli (def) löytää sen nimiavaruuden kautta ja päivittää sen.

toinen=> (defroutes foo #'hyvä-route #'huono-route)
#'toinen/foo
toinen=> (foo {:uri "/" :request-method :get})
{:status 200, :headers {}, :body ""}

;; päivitys näkyy heti
toinen=> (def hyvä-route (GET "/" [] {:status 200 :body "parempi"}))
#'toinen/hyvä-route
toinen=> (foo {:uri "/" :request-method :get})
{:status 200, :headers {}, :body "parempi"}

Nyt syy siihen, miksi päivitys näkyy heti, on eri. (defroutes) ei tee funktiota jossa on sisällä viittauksia var-olioihin, vaan laskee heti arvot. Sille parametriksi annetut funktiot eivät kuitenkaan ole enää suoraan funktio-olioita, vaan niihin osoittavia var-olioita. Ne käyttäytyvät samaan tapaan kuin osoittamansa funktiot, mutta niiden osoittamaa funktiota pystyy myös vaihtamaan nimiavaruuden kautta.

Toivon, että tämä yhteenveto vähän selvensi, miten clojuressa muuttujien uudelleenmäärittely toimii. Sen voinee tiivistää kahteen sääntöön:

  1. funktioiden määrittelyssä viitattujen nimien uudelleenmäärittely toimii aina.
  2. jos haluaa tietorakenteen, joka sisältää funktioita, näistä funktioista voi eksplisiittisesti tehdä sellaisia, että ne pystyy uudelleenmäärittelemään nimensä kautta.



kommentoi (viimeksi muutettu 26.08.2023 01:01)