[ANN] Julog.jl - Prolog-style logic programming in Julia

For anyone who’s wished they could do logic programming in Julia (but, like me, found miniKanren difficult to learn), I’m excited to announce Julog.jl, a package for Prolog-style logic programming in Julia! It just got added to the registry, so you can install it with:

] add Julog

It’s called Julog.jl rather than Prolog.jl because it doesn’t exactly implement Prolog syntax or semantics, but it’s very close, and also allows for tight integration with standard Julia code. Some neat features include:

  • Prolog-like syntax
  • Interpolation of expressions
  • Evaluation of custom Julia functions

Example

The @julog macro can be used to create logical terms or Horn clauses using Prolog-like syntax. It can also applied to a list of clauses to create a knowledge base. For example, the traditional Zen lineage chart can be encoded as:

clauses = @julog [
  ancestor(sakyamuni, bodhidharma) <<= true,
  teacher(bodhidharma, huike) <<= true,
  teacher(huike, sengcan) <<= true,
  teacher(sengcan, daoxin) <<= true,
  teacher(daoxin, hongren) <<= true,
  teacher(hongren, huineng) <<= true,
  ancestor(A, B) <<= teacher(A, B),
  ancestor(A, C) <<= teacher(B, C) & ancestor(A, B),
  grandteacher(A, C) <<= teacher(A, B) & teacher(B, C)
]

We can then query the knowledge base via SLD resolution:

Query: Is Sakyamuni the dharma ancestor of Huineng?
julia> sat, subst = resolve(@julog(ancestor(sakyamuni, huineng)), clauses);
julia> sat
true
Query: Who are the grandteachers of whom?
julia> sat, subst = resolve(@julog(grandteacher(X, Y)), clauses);
julia> subst
4-element Array{Any,1}:
  {Y => sengcan, X => bodhidharma}
  {Y => daoxin, X => huike}
  {Y => hongren, X => sengcan}
  {Y => huineng, X => daoxin}

If you really like Prolog syntax, you can also use the @prolog macro on a string that contains a Prolog program, which will then be parsed to a list of Julog clauses. However, not all Prolog syntax is currently supported, though the subset corresponding to Datalog should work fine.

Interpolation

You can interpolate Julia expressions when constructing Julog terms using the @julog macro. Julog supports two forms of interpolation. The first form is constant interpolation using the $ operator, where ordinary Julia expressions are converted to Consts:

Interpolating Julia expressions
julia> e = exp(1)
2.718281828459045
julia> term = @julog irrational($e)
irrational(2.718281828459045)
julia> dump(term)
Compound
  name: Symbol irrational
  args: Array{Term}((1,))
    1: Const
      name: Float64 2.718281828459045

The second form is term interpolation using the : operator, where pre-constructed Julog terms are interpolated into a surrounding Julog expression:

Interpolating Julog terms
julia> e = Const(exp(1))
2.718281828459045
julia> term = @julog irrational(:e)
irrational(2.718281828459045)
julia> dump(term)
Compound
  name: Symbol irrational
  args: Array{Term}((1,))
    1: Const
      name: Float64 2.718281828459045

Interpolation allows us to easily generate Julog knowledge bases programatically using Julia code:

julia> people = @julog [avery, bailey, casey, darcy];
julia> heights = [@julog(height(:p, cm($(rand(140:200))))) for p in people]
4-element Array{Compound,1}:
 height(avery, cm(155))
 height(bailey, cm(198))
 height(casey, cm(161))
 height(darcy, cm(175))

Custom Functions

In addition to standard arithmetic functions, Julog supports the evaluation of custom functions during proof search, allowing users to leverage the full power of precompiled Julia code. This can be done by providing a dictionary of functions when calling resolve. This dictionary can also accept constants (allowing one to store, e.g., numeric-valued fluents), and lookup-tables. E.g.:

Example custom functions
funcs = Dict()
funcs[:pi] = pi
funcs[:sin] = sin
funcs[:cos] = cos
funcs[:square] = x -> x * x
funcs[:lookup] = Dict((:foo,) => "hello", (:bar,) => "world")

@assert resolve(@julog(sin(pi / 2) == 1), Clause[], funcs=funcs)[1] == true
@assert resolve(@julog(cos(pi) == -1), Clause[], funcs=funcs)[1] == true
@assert resolve(@julog(lookup(foo) == "hello"), Clause[], funcs=funcs)[1] == true
@assert resolve(@julog(lookup(bar) == "world"), Clause[], funcs=funcs)[1] == true

More details on usage can be found in the README, and more examples are in the test folder. Let me know if you find this package useful, or have ideas for extensions or improvements! :slight_smile:

(Performance improvements would be especially welcome. I’ve tried all the standard tricks used in Prolog implementations, as well as the standard suggestions for Julia, but the benchmark tasks still don’t run as fast as in SWI-Prolog, and it’d be great to find out why! Even if the answer is just that an interpreter is always going to run slower.)

37 Likes

Hi !
Thank you for your great work.
I used to program in Prolog 40 years ago … but I’ve got some souvenirs.
I’m translating some educational programs from Python to Julia.
Now I’m busy with some lessons in elementary logic: Propositions of the first order.
I decided to use Julia+julog to check syntax of expressions.
I want to begin with ‘atomic’ props such as constants and variables. Connectors and delimiters will be added later.
Would-be propositions are supposed to be entered as strings using ! for negation, & for conjunction and | for disjonction, together with grouping delimiters, namely parentheses.
For atomic ones I decided to use only single capital letters.
The 2 usual constants will be “V” (French ‘vrai’ for true) and “F” (French ‘aux’ for false). All other letters such as A,B,C, X, Y etc… will be considered as variables.
So I began like this

using Julog

varP(f) = length(f) == 1 && f[1] in “ABCDEGHIJKLMNOPQRSTUWXYZ”

@julog Constante(“V”) <<= true # constante ‘vrai’
@julog Constante(“F”) <<= true # constante ‘faux’
@julog Variable(X) <<= varP(X)

This compiles without any problem. Ultimately I will add a lot of clauses under the form of rules, but to begin with I would lijke to test if V" and F" are recognized as constants and the other as variables.
So I wrote println(@julog Constante(“V”)) expecting a ‘true’ answer but no ! it just prints ‘(Constante V)’
and would print more or less the same I I would ask about K being a constant ???
I read doc hoping to find the answer (“How to ask if some fact is true ?”) .
Can you explain and answer.
Thanks a lot in advance.
Gilles

1 Like

To be more concise my question could be What is the Julog equivalent for Prolog usual query ?-

:slight_smile:

Glad you’ve decided to check Julog out! To perform a Prolog-style query with respect to a set of clauses, you should use the resolve function, i.e., resolve(query, clauses), which will return a tuple of two values, the first being whether the query was resolved/satisfied (which will be true or false), and the second being a list of variable substitutions that satisfied the query. So in your example, you’ll want write code like:

clauses = @julog [
    constante(“V”) <<= true,
    constante(“F”) <<= true,
    variable(X) <<= varP(X)
]

query = @julog constante("V")
sat, subst = resolve(query, clauses)

And you should find that sat is true. For a more detailed example, check out the Zen lineage example I used in the original post, and in the README.

3 Likes

Thank you very much for your quick answer. I hope I’ll find some time to test it tomorrow. Till now I have a working functional approach with Julia.

#les deux constantes Vrai et faux
constP(f) = length(f) == 1 && f[1] in "VF"
#variable, toute autre lettre majuscule que V ou F
varP(f) = length(f) == 1 && f[1] in "ABCDEGHIJKLMNOPQRSTUWXYZ"
#test de parenthèsage
inparP(f) = f[1] == '(' && f[length(f)] == ')'
#ramène l'expression entre parenthèses
betweenpar(f) = f[2:length(f)-1]

function parseP(f) #analyseur syntaxique
    if constP(f) # c'est une constante simple
        return f[1]
    end
    if varP(f) # c'est une variable simple
        return f[1]
    end
    if inparP(f) # l'expression est entre parenthèses'
        a = parseP(betweenpar(f))
        if a != nothing
            return a
        end
    end
    if f[1] == '!' # nous avons une négation
        a = parseP(f[2:end])
        if a != nothing
            return ['!', a]
        end
    end
    if '&' in f # nous avons une conjonction
        for i = 1:length(f)
            if f[i] == '&'
                a = parseP(f[1:i-1])
                b = parseP(f[i+1:end])
                if (a != nothing) && (b != nothing)
                    return ['&', a, b]
                end
            end
        end
    end
    if '|' in f #nous avons une disjonction
        for i = 1:length(f)
            if f[i] == '|'
                a = parseP(f[1:i-1])
                b = parseP(f[i+1:end])
                if (a != nothing) && (b != nothing)
                    return ['|', a, b]
                end
            end
        end
    end
    return nothing
end

I think it would be more simple and much more elegant to use inference in a prolog style, so julog is the thing.
In any case I will keep you informed about the result of my experiments.
Thank again, regards.
Gilles

Good morning !
I made the test as you suggested it works,
I can even ask for all constants subst will give [“V”,“F”].
Well, but …
To begin with, I must state that simple constants and variables are formulas.
but the clause identifying variables as we both suggest fails !
I mean that with this code :

using Julog

varP(X) = length(X) == 1 && X[1] in “ABCDEGHIJKLMNOPQRSTUWXYZ”

clauses = @julog [
constante(“V”) <<= true,
constante(“F”) <<= true,
variable(X) <<= varP(X),
formule(X) <<= constante(X),
formule(X) <<= variable(X)
]

query = @julog variable(“A”)
sat, subst = resolve(query, clauses)

println(sat)
println(subst)

I will receive false for sat and empty list for subst, and of course the test of “A” being a formula fails as well.

Glad you got the first part working! Because you’re using varP as a custom function, you have to tell that to resolve when you call it:

sat, subst = resolve(query, clauses, funcs=Dict(:varP => varP))

This is also covered in this section of the README.

If you have more questions, feel free to open them as a GitHub issue here: Issues · ztangent/Julog.jl · GitHub

works !
“A” is recognized as variable and as formula as well.
Thanks again for your quick and efficient help. OK ! now parse as formulas constants and variables, which is a good beginning waiting for more. :wink:
BTW I did it under Python using the lex Yacc (PLY) library. Trying to imitate, I couldn’t install the equivalent for Julia I will retry later, this is not the subject here.
I will continue tomorrow with new rules.
The context-free grammar describing formulas in logic of the first order is quite simple. I should complete more or less quickly.

I want to restart everything all over from scratch !
OK I tried

using Julog
clauses = @julog [
    constante(V) <<= true,
    constante(F) <<= true
 ]
#println(conj("A&B","A","B"))
query = @julog constante(X)
sat, subst = resolve(query, clauses )
println(sat)
println(subst)

Answer

true
Any[{X => #2}, {X => #3}]

what do these #2 and #3 stand for ?
I’m completely lost !

Sorry the above post is in the wrong place, you can delete it.

could any one come up with a simple example for subjective logic based on Julog.jl, please?

If by subjective logic you mean this, unfortunately Julog.jl doesn’t support reasoning about uncertain propositions or any kind of probabilistic reasoning. You might be interested in Problox.jl, a Julia wrapper for ProbLog. You could possibly also extend Julog.jl to support probabilistic logic in the style of ProbLog, and maybe even get subjective logic style reasoning by extending ProbLog with Beta-distributed priors (as in this paper).

2 Likes