Evaluating program against many different sets of facts
Hello,
I have a situation where I have a base program P with a set of clauses. This base program needs to be evaluated many times for different sets of facts A, B, C, ... The evaluation of the base program P against each set of facts happens concurrently in different goroutines. What would be a sensible way to achieve this? Ideally program P would be loaded from file just once, but then evaluated together with sets of facts A, B, C, ... The sets of facts are defined from go code by string formatting currently.
You can represent all facts for example in an association list, such as an AVL tree (see library(assoc), a public domain library written by Richard O'Keefe, available in many Prolog systems). You can pass around such a tree as an argument to the clauses, and the clauses would "look up" facts using the explicitly passed assocation list, instead of the implicit Prolog database.
In this way, the evaluation is completely thread-safe: Each thread can get passed its own assocation list that it uses to look up such facts. Lookup in AVL trees is O(log(N)) in the number N of facts, and can be made a bit more efficient maybe by using hash-tables if needed.
One additional comment about how to obtain such trees from strings: For this, it would help to provide the predicate read_from_chars/2 as in SICStus and Scryer Prolog, allowing us to read a Prolog term from a list of characters.
Sample use:
?- read_from_chars("f(a,b).", T).
T = f(a,b).
Once you have these facts as Prolog terms, you can store them in an AVL tree as mentioned, and look them up there.
How about creating instances for each A, B, C, ...?
prolog.Interpreter is not goroutine safe so we should keep them inside of their own goroutines and feed them facts via channels.
https://go.dev/play/p/zWzUth7nWu_L
multiple instances with shared/different facts
package main
import (
"fmt"
"github.com/ichiban/prolog"
)
func main() {
names := []string{"a", "b", "c"}
facts := make([]chan string, len(names))
queries := make([]chan string, len(names))
results := make(chan string, 1)
for i, n := range names {
facts[i] = make(chan string, 1)
queries[i] = make(chan string, 1)
go func(name string, fact, query <-chan string) {
p := prolog.New(nil, nil)
if err := p.Exec(`:- set_prolog_flag(unknown, fail).`); err != nil {
panic(err)
}
for {
select {
case f := <-fact:
if err := p.Exec(f); err != nil {
panic(err)
}
case q := <-query:
sol := p.QuerySolution(q)
switch err := sol.Err(); err {
case nil:
results <- name
case prolog.ErrNoSolutions:
break
default:
panic(err)
}
}
}
}(n, facts[i], queries[i])
}
// Shared facts/rules.
for i := range names {
facts[i] <- `
rice.
wasabi.
sushi(X) :- X, rice, wasabi.
`
}
// Individual facts.
facts[0] <- `tuna.`
facts[1] <- `salmon.`
facts[2] <- `egg.`
go func() {
for _, q := range queries {
q <- `sushi(salmon).`
}
}()
fmt.Printf("%s\n", <-results)
}
As of v0.15.0, you can implement read_from_chars/2 like this:
package main
import (
"bytes"
"errors"
"fmt"
"unicode/utf8"
"github.com/ichiban/prolog"
"github.com/ichiban/prolog/engine"
)
func main() {
p := prolog.New(nil, nil)
p.Register2(engine.NewAtom("read_from_chars"), readFromChars)
var s struct {
T prolog.TermString
}
if err := p.QuerySolution(`read_from_chars("f(a,b).", T).`).Scan(&s); err != nil {
panic(err)
}
fmt.Printf("T = %s\n", s.T)
}
func readFromChars(vm *engine.VM, chars, term engine.Term, k engine.Cont, env *engine.Env) *engine.Promise {
var b bytes.Buffer
iter := engine.ListIterator{List: chars, Env: env}
for iter.Next() {
switch r := iter.Current().(type) {
case engine.Atom:
// Atom is either a rune (r <= utf8.MaxRune) or an ID for the interned string (r > utf8.MaxRune).
if r > utf8.MaxRune {
return engine.Error(errors.New("not a character"))
}
_, _ = b.WriteRune(rune(r))
default:
return engine.Error(errors.New("not a character"))
}
}
if err := iter.Err(); err != nil {
return engine.Error(err)
}
s := engine.NewInputTextStream(&b)
return engine.ReadTerm(vm, s, term, engine.List(), k, env)
}
T = f(a,b)