首先,我們要先定義兩個詞彙:斷言與轉換函數。
even?
就是一個斷言。(even? 0)
;; => true
(even? 1)
;; => false
(filter even? (range 10))
;;=> (0 2 4 6 8)
add-2
就是一個轉換函數。我們也可以說,斷言是一種轉換函數的特例。(defn add-2 [x]
(+ 2 x))
(add-2 x)
;; => 4
(map add-2 [0 1 2 3 4 5])
;; => (2 3 4 5 6 7)
根據 Datalog 的文件,:where
開頭之後的子句有五種:(註1)
其中 not, not-join, or, or-join 子句已經在上一篇提到過了。表達式子句則有四種型態:
考慮如下的 SQL 查詢,它是查詢『 1984 年之前的電影』。
SELECT title
FROM movie
WHERE year < 1984;
如果用 Datalog 來改寫的話:
[:find ?title
:where
[?m :movie/title ?title]
[?m :movie/year ?year]
[(< ?year 1984)]]
在上頭的 Datalog 查詢裡, [(< ?year 1984)]]
是一個斷言表達式,它的傳回值只有「真」或是「假」兩種可能。當傳回真的時候,對應到的變數會被加入查詢結果裡;當傳回假的時候,對應到的變數則會從查詢結果裡加以移除。
這邊有兩點值得特別注意:
<, >, <=, >=, =, not=
都已內建在 Datalog 語言裡,開箱即可用。Datomic 資料庫可以儲存時間,而時間適合使用 :db.type/instant
資料型態來儲存。(註2) 資料庫儲存的時間型態,我們可以看成是 java.util.Date
,於是如果要對這個時間做加、減法時,我們需要先將其轉換為 java.lang.Long
型態的時間戳 (timestamp)。
下方是一個 Clojure 的函數,它可以根據生日時間 (birthday
) 與現在時間 (today
) 來算出年齡 (age)。
(defn age [birthday today]
(quot (- (.getTime today)
(.getTime birthday))
(* 1000 60 60 24 365)))
這邊解釋它的實作:
java.util.Date
) 分別是「出生時間」與「今日時間」,先用 .getTime
轉換這兩個時間為長整數資料型態的時間戳。quot
是除法。)一旦有了 age
函數,我們就可以透過 Datalog 查詢來算出人的年齡,下方的 Datalog 查詢是『給定人名與現在時間,用人名去查出這個人的出生時間,然後用 tutorial.fns/age
這個函數去算出這個人現在的年齡,並且傳回。』
[:find ?age
:in $ ?name ?today
:where
[?p :person/name ?name]
[?p :person/born ?born]
[(tutorial.fns/age ?born ?today) ?age]]
在上頭的 Datalog 查詢裡,[(tutorial.fns/age ?born ?today) ?age]]
是一個函數表達式,它的形式是 [(<fn> <arg1> <arg2> ...) <result-binding>]
。它會把函數運算 (<fn> <arg1> <arg2> ...)
的結果,綁定到 <result-binding>
這個變數裡。
這邊有三點值得特別注意:
clojure.core
這個命名空間裡的純函數,都可以拿來當函數使用,除了 eval
之外。tutorial.fns
)有 SQL 使用經驗的讀者,可能會想到執行環境 (runtime) ,因而覺得好像 Datalog 的「自訂斷言」與「自訂函數」有點太靈活了,不像是真的。在 SQL 資料庫,如果我們要使用自訂函數 (user-defined function)、儲存程序 (stored procedure) 的話,我們都必須專程把這些東西,安裝到資料庫裡,否則資料庫的執行環境無法調用它們。這是因為 SQL 資料庫與後端程式 (backend program) 通常是兩個獨立的執行環境。
然而,在上述的範例裡,卻沒有『安裝自訂函數』的步驟,彷彿後端程式與 Datomic 資料庫是在同一個執行環境裡一樣?
這個現象跟 Datomic 獨樹一格的軟體架構有關。在 Datomic 資料庫,因為讀寫完全分離,所以讀的部分即資料庫的查詢,它運作的執行環境是在後端程式裡。這就是為什麼我們使用自訂函數、自訂斷言,卻不需要做對應的安裝的原因。
註: