prolog icon indicating copy to clipboard operation
prolog copied to clipboard

Evaluating program against many different sets of facts

Open riccardopinosio opened this issue 3 years ago • 4 comments

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.

riccardopinosio avatar Mar 14 '22 18:03 riccardopinosio

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.

triska avatar Mar 14 '22 19:03 triska

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.

triska avatar Mar 14 '22 20:03 triska

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)
}

ichiban avatar Mar 16 '22 12:03 ichiban

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)

ichiban avatar Dec 16 '22 02:12 ichiban