trydatomic

Chapters

Rules

Rules let you name and reuse a group of :where clauses. Think of them as parameterised predicates: once defined, a rule can be invoked anywhere in a query just like a built-in clause. They keep complex queries readable and make shared logic easy to compose.

Defining and using a rule

A rule is a collection of definitions. Each definition is a vector whose first element is the head (the rule name and its parameters, like (strong? ?e)), followed by the body: the clauses that must match. Rules are passed to the query as an input, using the special % binding, and invoked in :where like any other clause. In Clojure that looks like this:

(def rules '[[(strong? ?e) [?e :stat/attack ?attack] [?e :stat/speed ?speed] [(> ?attack 80)] [(> ?speed 80)]]]) (d/q '[:find ?name :in $ % :where (strong? ?e) [?e :pokemon/name ?name]] db rules)

In the editor below the rules are the input after the query, so you can edit them too. This rule is true for a Pokemon when both its attack and its speed are above 80:

Query
[:find ?name :in $ % :where (strong? ?e) [?e :pokemon/name ?name]] ;; inputs, in the order of :in (after $) [[(strong? ?e) [?e :stat/attack ?attack] [?e :stat/speed ?speed] [(> ?attack 80)] [(> ?speed 80)]]]

Rules become especially valuable when the same set of clauses is needed in multiple queries, or when you want to give a complex condition a meaningful name.

Multiple definitions: rule-level OR

A rule name can have more than one definition, sometimes called branches. Datomic treats them as a disjunction: the rule succeeds if any definition's body matches. This gives you named OR logic without repeating or-join everywhere:

Query
[:find ?name :in $ % :where (fast-or-strong? ?e) [?e :pokemon/name ?name]] ;; inputs, in the order of :in (after $) [[(fast-or-strong? ?e) [?e :stat/speed ?speed] [(> ?speed 110)]] [(fast-or-strong? ?e) [?e :stat/attack ?attack] [(> ?attack 110)]]]

Recursive rules

A rule can invoke itself, giving you recursive logic that Datomic evaluates to a fixpoint: it keeps applying the rule until no new facts are derived. The canonical example is reachability in a graph.

In our data, :evolution/next links each Pokemon to the ones it evolves into. A recursive rule can follow those links as far as they go. The first definition is the base case, a direct evolution. The second says that a Pokemon also evolves into whatever its evolution evolves into:

Query
[:find ?name :in $ % :where [?start :pokemon/name "Bulbasaur"] (evolves-into? ?start ?descendant) [?descendant :pokemon/name ?name]] ;; inputs, in the order of :in (after $) [[(evolves-into? ?a ?b) [?a :evolution/next ?b]] [(evolves-into? ?a ?c) [?a :evolution/next ?b] (evolves-into? ?b ?c)]]

Ivysaur is a direct evolution, and Venusaur is reached through Ivysaur.

One important role: a recursive rule needs a non-recursive definition, a base case, to produce any results at all. A rule with only a recursive definition has nothing to recurse from, so it derives nothing. Termination isn't the base case's job: Datomic evaluates rules to a fixpoint regardless of shape, even one that cycles back on itself, stopping once a full pass through all definitions derives no new results.

Rules run in both directions

By default, a rule doesn't say which side is the input, though Datomic lets you declare required bindings on a rule if you want to enforce one. The same evolves-into? rule can start from the descendant and find its ancestors:

Query
[:find ?name :in $ % :where [?end :pokemon/name "Venusaur"] (evolves-into? ?ancestor ?end) [?ancestor :pokemon/name ?name]] ;; inputs, in the order of :in (after $) [[(evolves-into? ?a ?b) [?a :evolution/next ?b]] [(evolves-into? ?a ?c) [?a :evolution/next ?b] (evolves-into? ?b ?c)]]

Rules over relationships

In the Relationships chapter we built a query to find the Pokemon that hit Gengar super-effectively. Here it is again, all seven clauses of it:

Query
[:find ?name :where [?gengar :pokemon/name "Gengar"] [?gengar :pokemon/type ?defending-name] [?defending :type/name ?defending-name] [?attacking :type/strong-against ?defending] [?attacking :type/name ?attacking-name] [?e :pokemon/type ?attacking-name] [?e :pokemon/name ?name]]

The three clauses in the middle say one thing: a type is strong against another. A rule can give them a name. type-beats? takes two type names, and the rule finds the entities. The query is shorter and returns the same Pokemon:

Query
[:find ?name :in $ % :where [?gengar :pokemon/name "Gengar"] [?gengar :pokemon/type ?defending-name] (type-beats? ?attacking-name ?defending-name) [?e :pokemon/type ?attacking-name] [?e :pokemon/name ?name]] ;; inputs, in the order of :in (after $) [[(type-beats? ?attacking-name ?defending-name) [?defending :type/name ?defending-name] [?attacking :type/strong-against ?defending] [?attacking :type/name ?attacking-name]]]

A rule can use other rules. super-effective? takes an attacker and a defender, both Pokemon, and asks type-beats? about their types. Now the query reads like a sentence:

Query
[:find ?name :in $ % :where [?gengar :pokemon/name "Gengar"] (super-effective? ?e ?gengar) [?e :pokemon/name ?name]] ;; inputs, in the order of :in (after $) [[(type-beats? ?attacking-name ?defending-name) [?defending :type/name ?defending-name] [?attacking :type/strong-against ?defending] [?attacking :type/name ?attacking-name]] [(super-effective? ?attacker ?defender) [?attacker :pokemon/type ?attacking-name] [?defender :pokemon/type ?defending-name] (type-beats? ?attacking-name ?defending-name)]]

A rule is an ordinary clause, so it combines with the others, including negation. Gengar is Ghost and Poison. Which of the Pokemon that beat it are not Poison type?

Query
[:find ?name :in $ % :where [?gengar :pokemon/name "Gengar"] (super-effective? ?e ?gengar) (not [?e :pokemon/type "Poison"]) [?e :pokemon/name ?name]] ;; inputs, in the order of :in (after $) [[(type-beats? ?attacking-name ?defending-name) [?defending :type/name ?defending-name] [?attacking :type/strong-against ?defending] [?attacking :type/name ?attacking-name]] [(super-effective? ?attacker ?defender) [?attacker :pokemon/type ?attacking-name] [?defender :pokemon/type ?defending-name] (type-beats? ?attacking-name ?defending-name)]]

Rules with function clauses

The body of a rule can hold any clause, including the function clauses we saw earlier. This rule computes a Pokemon's base stat total, and exposes it as its second parameter. Then we can use it twice in one query, to compare Charizard with everyone else:

Query
[:find ?name ?total :in $ % :where [?charizard :pokemon/name "Charizard"] (base-total ?charizard ?charizard-total) (base-total ?e ?total) [(> ?total ?charizard-total)] [?e :pokemon/name ?name]] ;; inputs, in the order of :in (after $) [[(base-total ?e ?total) [?e :stat/hp ?hp] [?e :stat/attack ?attack] [?e :stat/defense ?defense] [?e :stat/sp-attack ?sp-attack] [?e :stat/sp-defense ?sp-defense] [?e :stat/speed ?speed] [(+ ?hp ?attack ?defense ?sp-attack ?sp-defense ?speed) ?total]]]

Charizard's total is 534. These are the Pokemon with a higher one.

TRY

Change "Bulbasaur" to "Eevee" in the recursive query to see it branch. Or swap the two arguments of super-effective? in the first Gengar query above, to ask which Pokemon Gengar hits super-effectively. Or edit the strong? rule to require both attack > 90 and speed > 90, and see how many Pokemon meet both criteria.

You can now
  • Name a group of :where clauses as a rule, and invoke it like a built-in clause.
  • Give a rule multiple definitions for OR logic, or make it recursive with a base case.
  • Compose rules from other rules, and use them alongside negation and function clauses.