Local Predicates

Flix supports an abstract mechanism called local predicates. A local predicate, like a local variable, is not visible to the outside.

To understand local predicates, consider the following example: We can a write a function that returns a Datalog program value which computes whether a graph has a cycle:

def cyclic(): #{Edge(Int32, Int32), Path(Int32, Int32), Cyclic()} = #{
    Path(x, y) :- Edge(x, y).
    Path(x, z) :- Path(x, y), Edge(y, z).
    Cyclic() :- Path(x, x).
}

def main(): Unit \ IO = 
    let db = #{
        Edge(1, 2).
        Edge(2, 3).
        Edge(3, 1).
    };
    query db, cyclic() select true from Cyclic() |> println

Here the cyclic function returns a Datalog program value which consists of three rules that compute the transitive closure of a graph of edges and whether there is path from a vertex to itself. We use the cyclic function inside main to determine if a small graph, given by db, has a cyclic. The program prints Vector#{true} when compiled and run.

Returning to cyclic, we see that its type is:

def cyclic(): #{Edge(Int32, Int32), Path(Int32, Int32), Cyclic()} = ...

This is sensible because the Datalog program value uses the predicate symbols Edge, Path and Cyclic with their given types. However, if we think more about it, we can realize that the Path predicate is really local to the computation: We are not meant to see it from the outside; it is an implementation detail! What we really want is that Edge(Int32, Int32) should be an input and Cyclic() should be an output. More importantly, Path(Int32, Int32) should not be visible from the outside nor part of the type. We can achieve this with predicate abstraction:

def cyclic(): #{Edge(Int32, Int32), Cyclic()} = 
    #(Edge, Cyclic) -> #{
        Path(x, y) :- Edge(x, y).
        Path(x, z) :- Path(x, y), Edge(y, z).
        Cyclic() :- Path(x, x).
    }

Here we use the syntax #(Edge, Cyclic) -> v to specific that only the predicates Edge and Cyclic from within v should be visible to the outside. Thus we can omit Path(Int32, Int32) from the return type of cyclic. Moreover, the Datalog program value no longer contains a Path predicate symbol that can be referenced. We can evidence this by observing that the program:

def main(): Unit \ IO = 
    let db = #{
        Edge(1, 2).
        Edge(2, 3).
        Edge(3, 1).
    };
    query db, cyclic() select (x, y) from Path(x, y) |> println

prints the empty vector Vector#{} since Path has been made local by the predicate abstraction.