Parenteser

Betraktninger fra Mat-teamets grønne enger

En liten titt på Datalog

Datalog er et logisk spørrespråk som ligger et sted mellom Prolog og SQL, noe som unektelig høres ganske esoterisk ut, men som viser seg å være ganske greit når man har tatt en liten titt på det.

Kode vs spørring

I en tidligere bloggpost i denne serien så vi dette kodeeksempelet:

(:poststed/navn
 (d/entity db [:poststed/postnummer "1630"]))

;; => "Gamle Fredrikstad"

Datomic støtter også å finne data ved hjelp av Datalog, hvor spørringen ville sett slik ut:

[:find ?navn .
 :where
 [?poststed :poststed/postnummer "1630"]
 [?poststed :poststed/navn ?navn]]

Hva skjer her egentlig? Jo, vi ber om å finne én verdi, som vi kaller ?navn.

  • Spørsmålstegnet i starten av symbolet indikerer til Datalog at dette er en variabel.
  • Punktumet . indikerer at vi bare vil ha én verdi, ikke flere.

Så ramser vi opp noen klausuler som må oppfylles. Disse følger mønsteret vi kjenner fra første bloggpost i serien:

[entitet, attributt, verdi]

Altså,

  • Finn en entitet ?poststed med attributtet :poststed/postnummer med verdi "1630".

  • Finn denne samme entiteten ?poststed sitt attributt :poststed/navn, og bind ?navn til verdien av attributtet.

Så kan vi be Datomic gjennomføre spørringen, slik:

(d/q '[:find ?navn .
       :where
       [?poststed :poststed/postnummer "1630"]
       [?poststed :poststed/navn ?navn]]
     db)

;; => "Gamle Fredrikstad"

Her kaller vi funksjonen datomic.api/q med spørringen vår og databasen db, og får svaret tilbake. Fortsatt Gamle Fredrikstad!

En forvikling i datamodellen

I tiden siden den forrige bloggposten ble skrevet, så har datamodellen vår endret seg. Vi oppdaget at “poststed” var en fullverdig entitet som kunne ha flere postnummere.

Altså:

  • Et poststed er identifisert av sitt navn.
  • Hvert poststed kan ha mange postnummere.

Med denne oppdaterte modellen, så ser spørringen vår sånn ut:

(d/q '[:find ?navn .
       :where
       [?postnummer :postnummer/nummer "1630"]
       [?postnummer :postnummer/poststed ?poststed]
       [?poststed :poststed/navn ?navn]]
     db)

;; => "Gamle Fredrikstad"

Her ser vi at det er tre klausuler.

  • Det finnes en entitet ?postnummer med :postnummer/nummer med verdi "1630".
  • Den samme entiteten ?postnummer har en referanse til en annen entitet ?poststed via :postnummer/poststed-attributtet. Det er en slags join.
  • Denne andre entiteten ?poststed har et ?navn.

Merk at alle variablene er navn som vi har funnet på. De kunne like gjerne vært ?a, ?b og ?c:

(d/q '[:find ?a .
       :where
       [?b :postnummer/nummer "1630"]
       [?b :postnummer/poststed ?c]
       [?c :poststed/navn ?a]]
     db)

;; => "Gamle Fredrikstad"

Poenget her er at de brukes til å vise hvilke deler av [e, a, v]-triplene som er de samme. De binder klausulene sammen.

Parameteriserte spørringer

Vi kan også lage oss en spørring som tar imot postnummeret. Det kan jo tenkes at vi lurer på noen andre steder også. Da ser det slik ut:

(d/q '[:find ?navn .
       :in $ ?nummer
       :where
       [?postnummer :postnummer/nummer ?nummer]
       [?postnummer :postnummer/poststed ?poststed]
       [?poststed :poststed/navn ?navn]]
     db "1630")

;; => "Gamle Fredrikstad"

I tillegg til :find og :where, så har vi nå :in. Denne seksjonen beskriver inn-parameterene. Legg merke til at d/q tar to parametere i tillegg til selve spørringen: db og "1630". Dette speiles av $ og ?nummer. Dollartegnet er altså databasen. Mer om det i en senere bloggpost.

Nå som vi har parameterisert spørringen, kan vi lage oss en funksjon:

(defn finn-stedsnavn-på-postnummer [db nummer]
  (d/q '[:find ?navn .
         :in $ ?nummer
         :where
         [?postnummer :postnummer/nummer ?nummer]
         [?postnummer :postnummer/poststed ?poststed]
         [?poststed :poststed/navn ?navn]]
       db nummer))

(finn-stedsnavn-på-postnummer db "1605") ;;=> "Fredrikstad"
(finn-stedsnavn-på-postnummer db "1412") ;;=> "Sofiemyr"

Men la meg stille deg et spørsmål: Er det egentlig nødvendig å pakke denne fine datastrukturen inn i en opak funksjon? Er ikke noe av poenget med et spørrespråk at det er deklarativt og inspiserbart? At det er data?

La oss prøve på det:

(def finn-stedsnavn-på-postnummer
  '[:find ?navn .
    :in $ ?nummer
    :where
    [?postnummer :postnummer/nummer ?nummer]
    [?postnummer :postnummer/poststed ?poststed]
    [?poststed :poststed/navn ?navn]])

(d/q finn-stedsnavn-på-postnummer db "1630") ;;=> "Fredrikstad"
(d/q finn-stedsnavn-på-postnummer db "1412") ;;=> "Sofiemyr"

Rock ’n roll! 🤘

En sjokkerende tvist på slutten

Hva om vi har lyst til å finne alle postnummere på et poststed?

(def finn-postnummere-for-stedsnavn
  '[:find [?nummer ...]
    :in $ ?navn
    :where
    [?postnummer :postnummer/nummer ?nummer]
    [?postnummer :postnummer/poststed ?poststed]
    [?poststed :poststed/navn ?navn]])

(d/q finn-postnummere-for-stedsnavn db "Gamle Fredrikstad")

;;=> ["1636" "1634" "1633" "1632" "1630" "1639" "1637"]

Hvordan skiller denne spørringen seg fra den forrige?

  • Istedenfor å finne ?navn . (ett navn), så finner vi nå [?nummer ...] (flere nummere).

  • Vi tar imot ?navn som parameter istedefor ?nummer.

Men hva med klausulene?

De er sørenmeg kliss like!

Vi beskriver jo den samme sammenhengen, bare med en annen ukjent.

M. Night Shyamalan, ta deg en bolle!

Magnar

Om Clojure og Datomic

Smakebiter av Datomic er en serie med bloggposter om den spennende og rare databasen Datomic. Lyst til å lese videre? Her er det neste innlegget i serien:

Transaksjoner i Datomic: Et herlig bekvemmelig API

Man har ikke mye glede av en database hvis man ikke kan dytte data inn i den. Etter 7 bloggposter i serien er det på tide å ta en titt på Datomic transaksjoner - og hva enn Dead Kennedys har med saken å gjøre.