Function canrun::goals::not

source ·
pub fn not(goal: impl Goal) -> Not
Expand description

Create a Goal that only succeeds if the sub-goal is proved to always fail.

This is implented using my interpretation of Negation as failure. When created, it will exhaustively run the sub-goal. If every possible iteration fails, it assumes that no additional facts can change the result and so marks the outer goal as a success. If any result state is a success, it adds a constraint that will continue checking until the sub-goal has no unresolved variables or open constraints. At this point, if it succeeds at least once, the outer Not will fail.

Examples

use canrun::{State, Query, LVar, all, any, unify, not};

let x = LVar::new();
let goal = all![
    any![unify(x, 1), unify(x, 2)],
    not(unify(x, 1)),
];
let results: Vec<_> = goal.query(x).collect();
assert_eq!(results, vec![2]);

Caveats

This is a somewhat recurring complication in the logic programming world, and I can’t claim a very deep understanding of the space. I have not yet found this approach to yield incorrect results, but, well… proving a negative is hard!

Performance considerations

This goal will do a speculative fork of the outer state in an attempt to search for success states. It will short circuit as soon as one is found, but this could be a lot of computation depending on the complexity of the outer state.

A not() that depends on unresolved variables should work correctly, but will require adding a constraint watch on these variables. If they are only resolved within branches of a fork, this could involve a lot of repeating forking before the not() is able to conclusively prove or disprove the sub-goal.

All of this is not to discourage usage, but just to say that you should try to keep them relatively simple and as precise as possible.