version 0.1.0 This post will explain the most common activity in Praxis; making rules, and also how the visual logic works. Try Praxis live, at: https://toblotron.com/praxis/0.1.0/ (Not mobile friendly)
How to make a rule
Rules in Praxis are constituted of different shapes that are connected together, or placed inside each other.
To create the most basic kind of rule, just drag out a Parameter shape from the palette and fill it with the rule name and arguments.
Shapes can always have connectors going in or out at the top or the bottom. If there is a Parameter shape with no connection coming in from the top (and which is not contained in a Group), Praxis will make that shape into the “head” of a new rule.
If there is nothing connected to the bottom of the Parameter shape, the rule will have no “body”, and we will call it a “fact”. (though usually we prefer placing our facts in tables, since they are so simple).
Here we have added a “body” to the head of our rule.
Order of execution within a rule
So, when we have a bunch of shapes connected – how is it decided in which order they will execute?
Praxis uses a simple depth-first order of execution, going from left to right if there is more than one child. Let’s look at a simple rule:
- We ask the question ancestors(Ancestor, Descendant) – this head matches the query, so we start executing the rule. There is only one shape below;
- It’s an OR-connector, which means that we will consider it true as soon as one of its branches is proven true. We test the two “child” branches from left-to right, and arrive at..
- A check for a parent(Ancestor,Descendant). If this succeeds the rule has finished, since the OR-connection above will be satisfied, but let’s assume we don’t, so we can go on and test the sibling branch:
- Here we ask for some Other child of Ancestor, and if we find one we go on to..
- The final shape to be tested. If this call succeeds, the rule is successful.
Order of execution among rules
Usually in Prolog we have several instances of a rule, and they are typically executed in the top-down order they appear in the source-file (text).
In Praxis, we go from left to right, when order among rules on the same page is concerned. Rules that are defined on an earlier page, in the model, will run before instances of the same rule defined on later pages.
Here we see a more common and “Prolog” way of defining the ancestor/2 rule.
Now we have two separate “cases” of the rule. The rule to the left will be tested first, and the rule to the right will be tested if the first rule fails. This should lead to exactly the same response as with the previous “OR”-formulation.
Groups and Connectors
If you only draw (normal) connections between normal shapes, you can only draw rules in which every contained/ connected shape must succeed.
A normal connection between shapes basically states “if the shape above [the connection] succeeded, it is now time to see if the shape below succeeds”, or “if above, then below”.
This is not very flexible, so let’s introduce ways of combining shapes with operators other than the basic “if-then” connections; groups and connectors.
Above we saw an example of the connector-shape, with the “OR” operator selected. Here is the same rule, using a Group instead. (the generated code should in this case be exactly the same)
In what order will shapes be executed within a Group? Well – if a shape (within a group) has no incoming connections, it will be one of the “head” shapes within that Group; it will denote the start of one of the alternative branches (or “siblings”) within that group. If a shape within a Group Has an incoming connection, it will not be executed until the shape connected to it from above has been proven successful.
The order of execution here is exactly like in the example with the OR-connector, above.
Groups can also contain other, smaller Groups.
One neat thing about Groups is that there can (like for all other shapes) be connections below them, which means that those shapes connected below will be executed After the Group has succeeded. (if it does)
PS – this is not a proper way for a reasonable person to solve “fizzbuzz” -it is just meant as an illustrative example.
Groups (as opposed to connectors, as we will soon see) have only 3 operators to choose from;
- AND – which is the default; ALL branches inside the Group must succeed
- OR – which means that ANY, at least One branch must succeed
- 1ST – or “First”, which means that once a branch succeeds, no other alternative branches will be tried on backtracking. (this interpretation uses the Prolog “if” operator which performs a cut)
PS – I’m not sure if using the “if”, or “->;” construct is a good idea, here – I’ve been looking at alternatives that do not include “cut”, but haven’t landed an any yet. -Keep in mind that it’s early days yet, for Praxis, and very few things are carved in stone
Connectors have more operators you can choose from!
In addition to the ones used by Groups, (AND, OR, 1ST) they can also perform:
- NOT – This will succeed if all shapes connected below are Not successful. This also uses the “if” operator which performs cuts.
- CUT – or just “!”, in Prolog-speak. This operator has the effect that no other alternatives to decisions taken previously in the rule will be tried during backtracking, once it hits. CUT will always succeed.
One of the objectives with this project is to explore the possibilities offered by expressing logic in a 2-dimensional way. One, experimental feature, is the “Else-connection”, which we will have a look at now. (just right-click a normal connection to turn it into an else-connection)
As we’ve said before, the meaning of the “normal” connection (solid blue) is “if the shape above me succeeds, then also try the shape below me”.
The Else-connector (dashed red) instead says “if the shape above me Fails, then try the shape below me”
This makes it possible to draw a kind of decision-trees.
In the same way that All the children connected by blue connectors will be executed when their parent-shape succeeds, All children of red connections will be executed when their parent-shape fails.
In the code, this is (right now) implemented through the use of the “if” (->;) construct, which I am not 100% in love with, since I guess it might lead to strange situations.. do you have any other idea that might work for this? 🙂
Currently, the else-connector only looks at the specific shape above it, and reacts if the statement of that shape is proven untrue. There could also be a variant which looks at the failure of the entire “branch” of the above shape. There remains a lot of experimentation to be done – please tell me what you think!