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:
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:
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:
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:
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:
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:
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:
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:
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?
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:
Charizard's total is 534. These are the Pokemon with a higher one.
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.
- Name a group of
:whereclauses 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.