A Clojure library designed to provide support for graph rewriting based on a persistent graph store.
lein new gapetest
profiles.clj
to contain your neo4j connection info:{:dev {
:env {:db-url "http://localhost:7474/db/data/"
:db-usr "<your neo4j user name>"
:db-pw "<your neo4j password>"}}}
grape
dependency to your project.clj. Add the lein-environ
plugin, so that Leiningen can source environment variables from your profiles.clj file. If you want to use the browser-based REPL (Gorilla) for developing your graph transformation rules (recommended) also add the lein-gorilla
plugin. Your project.clj file will look similar to:(defproject grapetest "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
:url "https://www.eclipse.org/legal/epl-2.0/"}
:dependencies [[org.clojure/clojure "1.10.1"]
[leadlab/grape "X.X.X"]]
:plugins [[lein-environ "1.1.0"]
[org.clojars.benfb/lein-gorilla "0.6.0"]]
:profiles {:dev {}}
:repl-options {:init-ns grapetest.core})
Start it up Make sure the Neo4j database is running. Start a Gorilla REPL with lein gorilla
. Open the indicated work sheet. Enter (use 'grape.core)
to import the Grape and connect to Neo4J. (This may take a few seconds. If you are getting an exception, your database is not running or something is wrong with the connection details.)
Load grape enter (use 'grape.core)
in the Web repl to load Grape
Create a rule Enter the following to create a rule that creates a node labeled "Hello". (You should see a visualization of the rule after you entered it.)
(rule 'hello!
{:create (pattern
(node 'n1 {:label "Hello"}))})
Execute the rule by calling it: (hello!)
Use the Neo4J browser (http://localhost:7474/browser/) to see that a node was indeed created in the graph database. (enter a simple cipher query: match (n) return n;
Enter another rule that matches the existing "Hello" node and links it to a newly created "Grape" node.
(rule 'hello-grape!
{
:read (pattern
(node 'n1 {:label "Hello"}))
:create (pattern
(node 'n2 {:label "Grape"})
(edge 'e {:label "to" :src 'n1 :tar 'n2}))})
(hello-grape!)
and check with the Neo4J browser that the graph was indeed extended.Graph rewriting rules are defined using the rule
form. Rules consist of three parts:
:read
specifies the graph pattern to be matched in the host graph:delete
part specifies which graph elements from the matched host graph should be deleted:create
part specifies which graph elements should be created when the rule is appliedBelow is a simple rule to find a node of type "Person". The node
form is used to specify the node to be searched for. Node types are determined by their "label".
(rule 'find-person
{:read
(pattern
(node 'n {:label "Person"}))})
The visual representation of the above rule is given in the image below. (The visual representation is automatically generated when using Gorilla repl. It can also be generate manually by calling document-rule
- see at the end of this manual).
Since find-person
does change the graph in any way, it is also called a graph test or a graph query.
Rules (and graph tests) can be executed, simply by calling them. A call to (find-person)
will return false
if no Person node can be matched - or true otherwise. In case of an empty database, the call will return false
.
The following rule creates only one node (of type Person). It has an empty :read
and delete
part, so it matches any host graph and deletes nothing.
(rule 'create-jens!
{:create
(pattern
(node 'n {:label "Person" :asserts {:name "'Jens'"}}))})
The node
form is used to specify the node to be created. Grape currently supports only one (optional) type label for nodes, but multiple (optional) property definitions (asserts). Properties are defined using maps. Note that the value of the maps is always a clojure String that wraps the actual expression that defines the Grape property. Thus, if you need a String value in grape, you need to use (single) quotes within the clojure value string, as exemplified above.
The visual representation of the above rule is given in the image below. Here we use the popular "inline" notation of rewrite rules, where green coloured shapes mark those graph elements that are being created, i.e., graph elements that appear on the right hand side of the rule, but not on the left-hand side.
As defined in Example 0, the rule can be applied by simply calling it:
(create-jens!)
Since the rule has an empty :read
part, it always applies and always returns true
. Each time it is called, it creates a new node of type Person
with an attribute name
of value Jens
.
You can validate that nodes of type Person
are indeed created by calling the graph query find-person
defined above. It should now return true
to indicate that there is at least one match.
Graph tests can be used to return query results. For example, the above graph test find-person
can be used to return a sequence of all matches of the graph test. This is done by calling the matches
function after calling a graph test or rule:
(find-person)
(matches)
returns a sequence of matche:
({:nodes ({:data {:name "Jens"}, :metadata {:id 16, :label "Person"}}), :edges ()}
{:nodes ({:data {:name "Jens"}, :metadata {:id 62, :label "Person"}}), :edges ()}
{:nodes ({:data {:name "Jens"}, :metadata {:id 75, :label "Person"}}), :edges ()})
Each item in the above sequence is a valid match of the graph test. (In the above example, the rule create-jens!
was invoked three times - and thus created three nodes, leading to three possible matches for rule find-person
.
Query results can be visualized in Gorilla REPL with the view
function. For example
(view (matches))
displays the following image:
Note that the above visualization shows the union of all the (three) possible matches of find-person
. Matches can also be visualized individually, for example (view (first (matches)))
visualizes the first returned match, i.e., a single Person
node with id 16.
While a simple call to matches
returns the matches for the most recently executed rule, matches
also accepts a rule to be executed as a parameter. This allows using the Clojure threading macro for maximum readability, e.g., (-> find-person matches first view)
displays the first match of rule find-person
.
Our first example rule was not very versatile, since it could not generate different persons. This can be improved by using parameterized rules. The following rule is more generic, as it takes the name of the person to be created as a parameter (p).
(rule 'create-person! ['p]
{:create
(pattern
(node 'n {:label "Person" :asserts {:name "'&p'"}}))})
Formal parameters p must be actualized when the rule is applied. The rule application below creates a Person node with name "Flo". Note that the expression &p is replaced with the value of the actual parameter p at rule execution time.
(create-person! "Flo")
You can use (-> find-person matches view)
in Gorilla REPL to validate that a new person with name "Flo" was indeed created.
The the next rule has a read as well as a create part. It matches two Person nodes with the names given as formal parameters and creates a parent-of relationship between them.
(rule 'parent_of! ['p 'c]
{ :read (pattern
(node 'f {:label "Person" :asserts {:name "'&c'"}})
(node 'j {:label "Person" :asserts {:name "'&p'"}}))
:create (pattern
(edge 'e {:label "parent_of" :src 'j :tar 'f} )
)})
(parent_of! "Jens" "Flo")
The call to (parent_of! "Jens" "Flo")
should return true
if you have at least one Person node with name "Flo" and one Person node with name "Jens". However, note that in the above example, we have three Person notes with name "Jens". Therefore, there are three possible matches.
(count (matches))
3
The order of matches is non-deterministic and Grape uses the first one in the return list:
(-> (matches) first view)
The following rule is similar to Example 3.
(rule 'works_for! ['e 's]
{ :read (pattern
(node 'f {:label "Person" :asserts {:name "'&s'"}})
(node 'j {:label "Person" :asserts {:name "'&e'"}}))
:create (pattern
(edge 'e {:label "works_for" :src 'j :tar 'f} ))})
It works fine when the read part maps the two nodes in the pattern to two nodes in the host graph, for example:
(works_for! "Flo" "Jens")
However, we cannot use it to express sitations where a person is self-employed, e.g.,
(works_for! "Jens" "Jens")
(Note: We are assuming here that there is only one person with name "Jens", i.e., that the person's name is a unique identifier. In that case the above rule application will not find a valid match (and return nil
). This is because Grape's rule matching engine will search for isomorphic matches of the read pattern in the host graph. This means that the nodes / edges in the read pattern must match to distinct nodes / edges in the host graph. This matching semantics can be changed to homomorphic matches by adding the :homo keyword to the definition of the reader pattern:
(rule 'works_for! ['e 's]
{ :read (pattern :homo
(node 'f {:label "Person" :asserts {:name "'&s'"}})
(node 'j {:label "Person" :asserts {:name "'&e'"}}))
:create (pattern
(edge 'e {:label "works_for" :src 'j :tar 'f} ))})
The above rule allows us to express our "self-employment" example.
The following rule also deletes matched graph elements. In this case it replaces a "works_for" edge with a new "Contract" node and two edges.
rule 'rewrite_contract!
{ :read (pattern
(node 'n1)
(node 'n2)
(edge 'e {:label "works_for" :src 'n1 :tar 'n2}))
:delete ['e]
:create (pattern
(node 'n3 {:label "Contract" :asserts {:name "'Contract'" :with "n1.name"}})
(edge 'e1 {:label "employer" :src 'n3 :tar 'n2})
(edge 'e2 {:label "employee" :src 'n3 :tar 'n1}))})
Another interesting aspect about the above rule is that the create part of the rule copies an attribute from a graph element matched in the read part of the rule (n1.name
).
Consider the following rule whose purpose it is to "fire" an employee with a given name (by deleting the contract node).
(rule 'fire-employee! ['name]
{:read (pattern
(node 'emp {:label "Person" :asserts {:name "'&name'"}})
(node 'con)
(edge 'e {:label "employee" :src 'con :tar 'emp}))
:delete ['con]})
But what happens to the 'employee' edge e when the contract node con is deleted? It can't be left "dangling", as that would result in an invalid graph. Graph transformation systems may be based on different theoretical foundations. Algebraic theories for graph transformation systems may be based on different approaches, including the so-called double pushout (DPO) approach and the single pushout (SPO) approach. We won't dive into the theory here, but what is important at this point is that these approaches differ in their treatment of "dangling" edges during node deletion. DPO rules will disallow dangling edges while SPO rules resolve dangling edges by also deleting them from the host graph. The default rule semantics is SPO in Grape. However, a different rule semantics can be specified. The following rule is identical to the previous but specifies DPO semantics. Applying it to our host graph will not be allowed if the application would cause any dangling edges.
(rule 'fire-employee! ['name]
{:theory 'dpo
:read (pattern
(node 'emp {:label "Person" :asserts {:name "'&name'"}})
(node 'con)
(edge 'e {:label "employee" :src 'con :tar 'emp}))
:delete ['con]})
Negative applications conditions (NACs) are conditions that, if met, inhibit a rule from being applied. Consider the works_for!
rule from Example 4. You may want to specify that a 'works_for' edge is created between two persons only if there isn't already such an edge in the graph. A NAC can be used to accomplish this, as seen in the following rule:
(rule 'works_for2! ['e 's]
{ :read (pattern
(node 'f {:label "Person" :asserts {:name "'&s'"}})
(node 'j {:label "Person" :asserts {:name "'&e'"}})
(NAC 1
(edge 'e1 {:label "works_for" :src 'j :tar 'f} )))
:create (pattern
(edge 'e2 {:label "works_for" :src 'j :tar 'f} ))
})
NACs are specified using 'NAC' forms, which essentially specifiy graph patterns that, if matched in the context of the read part, will inhibit the application of the rule. Grape allows multiple NACs per rule. The visual representation of NAC's uses dashed borders, with different NACs rendered in different colours.
This example shows a rule with multiple (two) NACs. The rule creates a sole_employer relationship between an employee and an employer, if the employee works for only that single employer (and a sole_employer relationship does not yet exist).
(rule 'sole_employer! []
{ :read (pattern
(node 'f )
(node 'j )
(edge 'e1 {:label "works_for" :src 'j :tar 'f})
(NAC 1
(node 'f2)
(edge 'e2 {:label "works_for" :src 'j :tar 'f2})
)
(NAC 2
(edge 'e3 {:label "sole_employer" :src 'j :tar 'f} )))
:create (pattern
(edge 'e4 {:label "sole_employer" :src 'j :tar 'f} ))
})
Of course, rules can be used within Clojure programs. For example, a rule can be applied repeatedly using the while_ form
Given the following example rule that deletes any node, we can simply delete the entire graph using while
:
(rule 'delete-any-node!
{:read (pattern (node 'n))
:delete ['n]})
(while (delete-any-node!))
However, Grape provides special control structures that support transactions. This is explained in the next few examples.
Grape supports atomic transactions. Consider the following rule let_one_go!
as an example:
(rule 'let_one_go! ['employer]
{
:read (pattern
(node 'emp {:label "Person" :asserts {:name "'&employer'"}})
(node 'worker)
(edge 'e {:label "works_for" :src 'worker :tar 'emp}))
:delete ['e]})
This rule "fires" once employee of a named employer (by deleting the works_for
relationship).
Now consider the case where you want to define a function that fires two employees. If course, you could simply call let_one_go!
twice. However, if the employer only has one employee left to fire, only one would be let go. In some cases, we may want "all or nothing" semantics (ACID transactions). Grape provides this functionality with the transact
and apl
forms. Transactions are defined as follows:
(transact
(apl 'let_one_go! employer)
(apl 'let_one_go! employer))
The above form defines a transaction that applies let_one_go!
twice (if possible) or makes no change at all. A transaction is invoked with the attempt function, which returns true if (and only if) the entire transaction succeeds.
(attempt
(transact
(apl 'let_one_go! employer)
(apl 'let_one_go! employer)))
Of course, transactions can be used to define Clojure operations:
(defn fire-two!
[employer]
(attempt
(transact
(apl 'let_one_go! employer)
(apl 'let_one_go! employer))))
A Grape rule may have multiple possible applications in a host graph. Grape supports backtracking when working with transactions. Consider the following rules hire!
and promote!
as an example. The first rule (hire!
) recruits a worker on the job market for a given employer name
.
(rule 'hire! ['name]
{:read (pattern
(node 'm {:label "Employer" :asserts {:name "'&name'"}})
(node 'w {:label "Worker"})
(NAC
(edge 'e {:label "works_for" :src 'w :tar 'm})))
:create (pattern
(edge 'e {:label "works_for" :src 'w :tar 'm}))})
The second rule (promote!
) promotes one of the workers who work for employer name
to become a Director
, but only if that worker does not also work for a different employer.
(rule 'promote! ['name]
{:read (pattern
(node 'm {:label "Employer" :asserts {:name "'&name'"}})
(node 'w {:label "Worker"})
(edge 'e {:label "works_for" :src 'w :tar 'm})
(NAC
(node 'm2)
(edge 'en {:label "works_for" :src 'w :tar 'm2}))
)
:delete ['w]
:create (pattern
(node 'd {:label "Director"})
(edge 'f {:label "works_for" :src 'd :tar 'm})
)})
Now consider the following transaction hire_director!
that consists of hiring a worker and then promoting the worker to become a director.
(defn hire_director! [employer]
(transact
(apl 'hire! employer)
('promote! employer)))
In general, there will be many possible matches for hire!
. However, only those workers can be promoted to Director, which do not also work for a different employer. Therefore, transaction hire_director!
may need to backtrack in order to search for a worker that can be promoted. For example, consider the following job market that has four workers and four employers:
(rule 'setup-job-market!
{:create
(pattern
(node 'w1 {:label "Worker"})
(node 'w2 {:label "Worker"})
(node 'w3 {:label "Worker"})
(node 'w4 {:label "Worker"})
(node 'm1 {:label "Employer" :asserts {:name "'Jens'"}})
(node 'm2 {:label "Employer" })
(node 'm3 {:label "Employer" })
(node 'm4 {:label "Employer" })
(edge 'e1 {:label "works_for" :src 'w1 :tar 'm2})
(edge 'e2 {:label "works_for" :src 'w2 :tar 'm3})
(edge 'e3 {:label "works_for" :src 'w3 :tar 'm4}))})
Attempting to hire a Director for employer "Jens" (attempt (hire_director! "Jens"))
may attempt to hire any of the workers but only succeed with promoting worker w4
, as all other workers also work for other employers. Grape will find this only possible match by using backtracking.
bind
and consult
At times we may want to pass values from one rule application context to another. This can be done in Grape transactions using the bind
and consult
forms. Consider the same starting graph as in the previous example. Now consider the following rules hire-someone!
and train!
.
(rule 'hire-someone! ['name]
{:read (pattern
(node 'm {:label "Employer" :asserts {:name "'&name'"}})
(node 'w {:label "Worker"}))
:create (pattern
(edge 'e {:label "works_for" :src 'w :tar 'm}))})
(rule 'train! ['name 'w]
{:read (pattern
(node 'm {:label "Employer" :asserts {:name "'&name'"}})
(node 'w {:label "Worker"}))
:create (pattern
(edge 'f {:label "in_training" :src 'm :tar 'w})
)})
Note that the second rule (train!
) receives the worker node w
as a parameter. Let's now define a transaction that first hires someone and then "trains" that worker. That transaction must first apply the hire-someone!
rule and then pass the hired worker (node) as a parameter to the train!
rule. This can be done using bind
and consult
. bind
binds a node that was matched in a successfully applied rule to a given name and consult
uses the value bound to a given name as a parameter for another rule application. Here is the transaction:
(defn hire_and_train! [employer]
(transact
(apl 'hire-someone! employer)
(bind 'new-hire 'w)
(apl 'train! employer (consult 'new-hire))))
Until
Sometimes we may need additional control structures in transactions. For example, consider the following graph setup:
(rule 'setup-likes!
{:create
(pattern
(node 'n1)
(node 'n2 )
(node 'n3 )
(edge 'e1 {:label "likes" :src 'n1 :tar 'n2})
(edge 'e2 {:label "likes" :src 'n1 :tar 'n3})
(edge 'e3 {:label "likes" :src 'n2 :tar 'n1})
(edge 'e4 {:label "likes" :src 'n2 :tar 'n3})
(edge 'e5 {:label "likes" :src 'n3 :tar 'n2})
(edge 'e6 {:label "likes" :src 'n3 :tar 'n1}))})
Moreover, consider the following rule, which deletes a likes
relationship between two arbitrary nodes.
(rule 'dislike_one!
{:read (pattern
(node 'n1)
(node 'n2)
(edge 'e {:label "likes" :src 'n1 :tar 'n2}))
:delete ['e]})
Now let's assume that we want a transaction that repeatedly deletes likes
relationships until there is a unidirectional cycle of likes
relationships in the graph. Formally, this condition can be expressed in the following graph test:
(rule 'chain_of_likes?
{:read (pattern
(node 'n1)
(node 'n2 )
(node 'n3 )
(edge 'e1 {:label "likes" :src 'n1 :tar 'n2})
(edge 'e2 {:label "likes" :src 'n2 :tar 'n3})
(edge 'e3 {:label "likes" :src 'n3 :tar 'n1})
(NAC 1 :homo
(node 'n5)
(node 'n6)
(edge 'e5 {:label "likes" :src 'n5 :tar 'n6})
(edge 'e6 {:label "likes" :src 'n6 :tar 'n5})))})
This can be accomplished by using the Grape until
control structure. The first argument of an until
function is the completion condition (which must be side-effect free). Then then one or several Grape rules (or other control structures) can be called. Here is the program for the above example:
(attempt (until 'chain_of_likes? (apl 'dislike_one)))
Similar to loops in other programming languages, until
control structures may not necessarily terminate. Of course, the above example program quite clearly terminates, as each iteration removes a likes
edge from the graph - and the number of these edges is finite. However, in general, transactions that use until
may loop forever.
Choice
Sometimes we may want to try different rule applications non-deterministically. The choice
constrol structure can be used for this. Consider the following two rules:
(rule 'KimLikesJohn!
{:read
(pattern
(node 'n1 {:label "Kim"})
(node 'n2 {:label "John"}))
:create
(pattern
(edge 'e1 {:label "likes" :src 'n1 :tar 'n2}))})
(rule 'JohnLikesKim!
{:read
(pattern
(node 'n1 {:label "Kim"})
(node 'n2 {:label "John"}))
:create
(pattern
(edge 'e1 {:label "likes" :src 'n2 :tar 'n1}))})
and the following start graph:
(rule 'setup3!
{:create
(pattern
(node 'n1 {:label "Kim"})
(node 'n2 {:label "John"})
(edge 'e1 {:label "likes" :src 'n1 :tar 'n2}))})
The following program will non-deterministically choose one of the two above rules so that the graph test likeEachOther?
is met:
(attempt
(choice (apl 'KimLikesJohn!)
(apl 'JohnLikesKim!))
(apl 'likeEachOther?))
The graph test likeEachOther?
is defined as:
(rule 'likeEachOther?
{:read
(pattern
(node 'n1 {:label "Kim"})
(node 'n2 {:label "John"})
(edge 'e1 {:label "likes" :src 'n2 :tar 'n1})
(edge 'e2 {:label "likes" :src 'n1 :tar 'n2}))})
Avoid
Sometimes we may want to specify a condition to avoid in a transaction. To some degree, this can be achieved by using Negative Application Conditions (NACs) attached to rules (see above). However, the expressiveness of NACs is limited. Therefore, Grape provides the avoid
control structure.
Consider the following start graph
(rule 'setup4!
{:create
(pattern
(node 'n1)
(node 'n2 )
(node 'n3 )
(node 'n4 {:label "A"} )
(node 'n5 )
(edge 'e1 {:label "relates" :src 'n1 :tar 'n4})
(edge 'e2 {:label "relates" :src 'n2 :tar 'n4})
(edge 'e3 {:label "relates" :src 'n3 :tar 'n4}))})
and the following rule that creates a relates
relationship between an arbitrary node and the node with label "A".
(rule 'relate-one!
{:read
(pattern
(node 'n1)
(node 'n4 {:label "A"}))
:create
(pattern
(edge 'e1 {:label "relates" :src 'n1 :tar 'n4}))})
The following Grape program tries out all possible (4) matches for relate-one!
so that graph test double?
fails (i.e., is avoided).
(attempt (transact (apl 'relate-one!)
(avoid (apl 'double?))))
Graph test double?
is defined below:
(rule 'double?
{:read
(pattern
(node 'n1 )
(node 'n4 {:label "A"} )
(edge 'e1 {:label "relates" :src 'n1 :tar 'n4})
(edge 'e2 {:label "relates" :src 'n1 :tar 'n4}))})
We already saw how simple equality conditions on node and edge attributes can be expressed. For more complex conditions on attributes (for example inequalities), Grape provides a special condition
form in the read part of rules. Moreover, Grape provides an assign
form in the create part of rules to revise attribute values of machted graph elements. These two concepts are exemplified with the popular Ferryman problem. Consider a ferryman who is tasked to ship a goat, a grape and a wolf from one side of the river to the other side. The ferryman can only ship one thing at a time. Moreover, if left unsupervised, the wolf will eat the goat and the goat will eat the grape, respectively. The Ferryman problem is to find a sequence of actions to safely ship all three items to the other side. In Grape it can be described in the following transaction:
(until 'all_on_the_other_side?
(transact (choice (apl 'ferry_one_over!)
(apl 'cross_empty!))
(avoid (apl 'wolf-can-eat-goat?)
(apl 'goat-can-eat-grape?))))
The start graph for the problem is shown here:
(rule 'setup-ferryman!
{:create
(pattern
(node 'tg {:label "Thing" :asserts {:kind "'Goat'"}})
(node 'tc {:label "Thing" :asserts {:kind "'Grape'"}})
(node 'tw {:label "Thing" :asserts {:kind "'Wolf'"}})
(node 's1 {:label "Side" :asserts {:name "'This side'"}})
(node 's2 {:label "Side" :asserts {:name "'Other side'"}})
(node 'f {:label "Ferry" :asserts {:name "'Ferryman'" :coins "7"}})
(edge 'e1 {:label "is_at" :src 'tg :tar 's1})
(edge 'e2 {:label "is_at" :src 'tc :tar 's1})
(edge 'e3 {:label "is_at" :src 'tw :tar 's1})
(edge 'e4 {:label "is_at" :src 'f :tar 's1})
)})
... whereas the target condition of the transaction all_on_the_other_side?
is specified below:
(rule 'all_on_the_other_side?
{:read
(pattern :homo
(node 'tg {:label "Thing" :asserts {:kind "'Goat'"}})
(node 'tc {:label "Thing" :asserts {:kind "'Grape'"}})
(node 'tw {:label "Thing" :asserts {:kind "'Wolf'"}})
(node 's2 {:label "Side" :asserts {:name "'Other side'"}})
(edge 'e1 {:label "is_at" :src 'tg :tar 's2})
(edge 'e2 {:label "is_at" :src 'tc :tar 's2})
(edge 'e3 {:label "is_at" :src 'tw :tar 's2}))})
The two graph tests specifying the dangerous conditions to avoid are:
Now considering the above transaction definition (which uses an until
control structure), the ferryman has a choice to ship one thing over or cross empty at any given iteration. This means that it is perfectly possible to loop endlessly. For example, the ferryman could take the goat for rides back and forth forever. Or the ferryman may cross empty forever. In order to bound the search, each trip across the river costs a coin - and we give the ferryman initially a purse of 7 gold coins to start with. Each time the ferryman crosses, it needs to be checked whether the ferryman still has a coin left - and once he reaches the other side, the number of coins needs to be reduced. This is done with Grape condition
and assign
forms, respectively. The following example shows their use:
(rule 'ferry_one_over!
{:read
(pattern
(node 's1 {:label "Side"})
(node 's2 {:label "Side"})
(node 'f {:label "Ferry"})
(node 't {:label "Thing"})
(edge 'et {:label "is_at" :src 't :tar 's1})
(edge 'e {:label "is_at" :src 'f :tar 's1})
(condition "f.coins > 0"))
:delete ['e 'et]
:create
(pattern
(edge 'en {:label "is_at" :src 'f :tar 's2})
(edge 'et2 {:label "is_at" :src 't :tar 's2})
(assign "f.coins=f.coins-1"))})
The rule that defines empty crosses is specified similarly.
(rule 'cross_empty!
{:read
(pattern
(node 's1 {:label "Side"})
(node 's2 {:label "Side"})
(node 'f {:label "Ferry"})
(edge 'e {:label "is_at" :src 'f :tar 's1})
(condition "f.coins > 0"))
:delete ['e ]
:create
(pattern
(edge 'en {:label "is_at" :src 'f :tar 's2})
(assign "f.coins=f.coins-1"))})
Indeed, the ferryman needs at least 7 gold coins to carry out his task. In other words, the above transaction will fail with fewer crosses.
Grape implements checks for syntactical ans static semantical correctness and will through exceptions if errors are found during rule definition. For example the following rule is considered incorrect with respect to Grape's syntax definition, as the rule name is a string and not a symbol:
(rule "testrule"
{:create
(pattern
(node 'n {:label "Person" :asserts {:name "'Jens'"}}))})
An exception with the following message will be thrown in this case:
Grape syntax error: rule name must be a symbol
Expected syntax:
RULE :- ( rule NAME <[PAR+]> { <:theory 'spo|'dpo> <:read PATTERN> <:delete [ID+]> <:create PATTERN> } )
NAME, PAR, ID :- *symbol*
PATTERN := (pattern ...)
... where <> denotes an optional element, | denotes an alternative choice, and N+ denotes a list of elements
Likewise, here is an example for a syntactically correct rule that has problems with respect to static semantics. (In this case an undeclared identifier is referenced.) Consider the following simple example rules:
(rule 'testrule
{:create
(pattern
(node 'n {:label "Person" :asserts {:name "'@id'"}}))})
The exception thrown may look as follows:
Grape static analysis error: identifier id is used but not declared
Note, though, that Grape is schema-less, i.e., there is no need / ability to define a graph schema type for rules. Thus, Grape has no means of checking whether rule definitions are compliant to a particular graph class.
Grape supports automatic generation of rule visualizations based on the Graphviz tool. Each rule definition automatically creates a function to emit the rule in Graphviz (dot) format. The name of that function is -dot. For example, the following function call will return the visual prepresentation of the above example rule:
(use 'grape.visualizer)
(create-jens!-dot)
Visual representations can also be saved as image files to the file system by calling function document-rule
for a defined rule, or document-rules
for all defined rules:
(document-rule 'create-jens!) ; saves a PNG visual representation of rule 'createJens!
(document-rules) ; saves PNG visual representations for all defined rules
Copyright © 2016-20 Jens Weber
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.
Can you improve this documentation? These fine people already did:
Jens Weber & Simon DiemertEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close