In the previous chapter, we focused on compiling a recursive predicate with the recursive_compiler. However, many useful predicates are not recursive. These are handled by a different component of UnifyWeaver: the stream_compiler.
This chapter explores how UnifyWeaver efficiently transforms simple, non-recursive rules into streaming Bash pipelines.
stream_compilerThe stream_compiler.pl module is responsible for compiling any predicate that does not call itself (either directly or indirectly). Its philosophy is to treat Prolog’s logic as a blueprint for a data processing pipeline, where data flows from one command to the next, much like a Unix pipeline.
This approach is highly efficient in terms of memory, as it avoids loading large datasets into memory all at once. Instead, data is processed one line at a time.
Let’s consider the grandparent/2 rule from our family_tree.pl example:
grandparent(GP, GC) :-
parent(GP, P),
parent(P, GC).
Logically, this means “GP is a grandparent of GC if GP is a parent of P, and P is a parent of GC.” The variable P acts as the bridge, linking the two parent relationships.
The stream_compiler translates this into a Bash pipeline that mirrors the logic:
parent(GP, P) goal generates a stream of all known parent-child pairs.GP:P from the input stream, this stage finds all children GC where P is the parent. It then outputs the final GP:GC pair.This is a classic stream join. Here’s the actual generated code from UnifyWeaver:
parent_join() {
while IFS= read -r input; do
IFS=":" read -r a b <<< "$input"
for key in "${!parent_data[@]}"; do
IFS=":" read -r c d <<< "$key"
[[ "$b" == "$c" ]] && echo "$a:$d"
done
done
}
grandparent() {
parent_stream | parent_join | sort -u
}
How it works:
parent_stream outputs all parent:child pairsparent_joinsort -u removes duplicatesNote on variable scope: The variables a, b, c, d are local to the while loop. The pipe is safe here because we’re not accumulating state - just transforming each line. See Chapter 7 for details on when variable scope matters.
To compile a non-recursive predicate, you can use the compile_stream/3 predicate, which works just like compile_recursive/3.
Let’s try this from the SWI-Prolog prompt (assuming you have loaded the compiler and family_tree.pl):
?- compile_stream(grandparent/2, [], BashCode),
open('education/grandparent.sh', write, S), write(S, BashCode), close(S).
true.
This will create a new file, education/grandparent.sh, containing the Bash implementation for finding grandparents.
You can now run this script just as you did before:
$ source education/grandparent.sh
# Find all grandparents and their grandchildren
$ grandparent_all
grandparent:esau
grandparent:jacob
...
# Check if abraham is a grandparent of jacob
$ grandparent_check abraham jacob && echo "Yes" || echo "No"
Yes
What happens when a predicate has more than one rule? For example, let’s define a child/2 predicate.
% A person is a child if they have a mother or a father.
child(C) :- parent(M, C), female(M).
child(C) :- parent(F, C), male(F).
In Prolog, these two rules represent an “OR” condition. A goal child(X) will succeed if either rule succeeds.
The stream_compiler handles this by:
sort -u to remove any duplicate results.The conceptual Bash code would look like this:
child_stream() {
{
# Code for the first rule (mother)
generate_children_from_mothers;
# Code for the second rule (father)
generate_children_from_fathers;
} | sort -u
}
The use of sort -u is a simple and robust way to implement the logical OR and ensure that each distinct result appears only once, just as it would in Prolog.
compile_predicate WrapperInstead of manually choosing between compile_recursive/3 and compile_stream/3, you can use the main wrapper predicate compile_predicate/3. This predicate automatically analyzes the predicate and calls the correct underlying compiler.
?- compile_predicate(grandparent/2, [], BashCode). % Will use stream_compiler
?- compile_predicate(ancestor/2, [], BashCode). % Will use recursive_compiler
This is the recommended way to compile predicates unless you have a specific reason to invoke one of the compilers directly.
We have now covered both recursive and non-recursive compilation, the two main pillars of UnifyWeaver. You should now have a good understanding of how your Prolog rules, whether simple or complex, are transformed into executable scripts.
In the next chapter, we will explore some more advanced topics, including the constraint system that lets you control aspects of the compilation, such as ordering and uniqueness.
| ← Previous: Chapter 4: Your First UnifyWeaver Program | 📖 Book 2: Bash Target | Next: Chapter 6: Advanced Topic: The Constraint System → |