mndrix / golog

Prolog interpreter in Go
MIT License
374 stars 39 forks source link

Olegs/expose native objects #23

Closed wvxvw closed 7 years ago

wvxvw commented 7 years ago

The changes roughly fall into these categories:

mndrix commented 7 years ago

I finished reading all the changes. First off, I can tell that this took a lot of effort. Thanks for being willing to contribute it.

Can you isolate the pretty printing changes in one commit, the apropos/help changes in a second commit and send me a pull request for those two? If so, I should be able to merge it right away.

I love the idea of easy conversion between Go types and Prolog terms. The biggest question in my mind is how to represent a Go struct in idiomatic Prolog. Let's consider this type for conversation:

type Person struct {
    Name string
    Age int
}

The code in this pull request represents the Go value Person{Name:"Bob", Age: 27} with the following term:

person(name("Bob"),age(27))

I'm I were to represent that same value in idiomatic Prolog, without any thought for Go, I'd probably use person("Bob",27) with some accessor predicates like:

person_name(person(N,_),N).
person_age(person(_,A),A).

Losing the name wrapper around each Prolog argument makes it harder to convert from Prolog back into Go, but I think it gives us a more natural representation in Prolog.

Ideally, I think I'd like something along these lines. You may add field tags like golog:"foo,7" to assist with the conversion to/from Prolog. That uses the Prolog name foo and puts it in the 7th position in a compound term. Using the type above, and being explicit about the tags, we'd have:

type Person struct {
    Name string `golog:"name,1"`
    Age int `golog:"age,2"
}

Then something like term.NewCompound(val interface{}) *term.Compound as a convenience for converting Go values into Prolog terms. Along with term.Decode(t term.Term, dst interface{}) to converting Prolog terms into Go values. Both of these functions are probably built on top of more general Marshal and Unmarshal interfaces. Since all these functions are about Prolog terms, not their execution, I think they belong in the term package.

I'd also want to be able to do something like m := golog.NewMachine().Consult(&Person{}) to automatically add person_name/2 and person_age/2 definitions to my machine. It'd be especially cool if methods on *Person were converted into foreign predicates which called the method. Anyway, these helper predicates make it easy to work with Go values in Prolog in a natural way. If the Go types change, the Prolog helpers automatically change too so I don't have to rewrite my Prolog code.

If you're willing to refactor your native object support in this direction, I think we can get it to a point to merge as well.

wvxvw commented 7 years ago

Hi,

It shouldn't be a problem to separate the pretty printing and the help/apropos changes. I might even do it today, or if not, then over the weekend.

Wrt. serialization, here are some thoughts:

I didn't actually represent Go structs like person(name("Bob"), age(27)), it was person([name("Bob"), age(27)]), so the query to extract a value would usually look more like this:

person_name(person(X), Name) :- member(X, name(Name)).

In the project I worked on, Prolog was also used to represent configuration, and it would've been inconvenient to have to specify all the values (while there may have been defaults). It makes field value extraction slower and "kind of" nondeterministic, but speed wasn't a concern for me, and if someone was to write a configuration with multiple values for the same field--so bad for them. Say, I'm going to write the code for accessing field values, do you think it might be still better to have fields as a list rather than tuple?

Re' autogeneration of methods... it sounds doable, but Go's reflection is a minefield... I will try, but it's hard to tell if it is in fact doable before I can get it to work (or fail to). Also, when writing foreign predicates, I discovered that, at least for me, there were two useful patterns: (1) for a predicate foo/2, if the Go implementation could return error, I'd also create foo/1 which would fail if error was instantiated by foo/2. The problem with this, however, was that the way to figure out if foo/2 instantiated the error was very invasive (I needed to try to cast the result to []term.Term and then look for the variable I thought would be the error). (2) In a similar way, whenever there was a predicate taking context.Context as its first argument, I'd have one without, substituting context.Background() for the first argument. This is easier because I don't need to examine the values returned, but it's not a very common case, I think. So, say, I succeed in writing some generic code to invoke methods defined on Go object, do you think it's interesting / worthwhile to generate these extra predicates with less arguments?

On Wed, Mar 29, 2017 at 9:06 PM, Michael Hendricks notifications@github.com wrote:

I finished reading all the changes. First off, I can tell that this took a lot of effort. Thanks for being willing to contribute it.

Can you isolate the pretty printing changes in one commit, the apropos/help changes in a second commit and send me a pull request for those two? If so, I should be able to merge it right away.

I love the idea of easy conversion between Go types and Prolog terms. The biggest question in my mind is how to represent a Go struct in idiomatic Prolog. Let's consider this type for conversation:

type Person struct { Name string Age int }

The code in this pull request represents the Go value Person{Name:"Bob", Age: 27} with the following term:

person(name("Bob"),age(27))

I'm I were to represent that same value in idiomatic Prolog, without any thought for Go, I'd probably use person("Bob",27) with some accessor predicates like:

personname(person(N,),N). personage(person(,A),A).

Losing the name wrapper around each Prolog argument makes it harder to convert from Prolog back into Go, but I think it gives us a more natural representation in Prolog.

Ideally, I think I'd like something along these lines. You may add field tags like golog:"foo,7" to assist with the conversion to/from Prolog. That uses the Prolog name foo and puts it in the 7th position in a compound term. Using the type above, and being explicit about the tags, we'd have:

type Person struct { Name string golog:"name,1" Age int `golog:"age,2" }

Then something like term.NewCompound(val interface{}) *term.Compound as a convenience for converting Go values into Prolog terms. Along with term.Decode(t term.Term, dst interface{}) to converting Prolog terms into Go values. Both of these functions are probably built on top of more general Marshal and Unmarshal interfaces. Since all these functions are about Prolog terms, not their execution, I think they belong in the term package.

I'd also want to be able to do something like m := golog.NewMachine().Consult(&Person{}) to automatically add person_name/2 and person_age/2 definitions to my machine. It'd be especially cool if methods on *Person were converted into foreign predicates which called the method. Anyway, these helper predicates make it easy to work with Go values in Prolog in a natural way. If the Go types change, the Prolog helpers automatically change too so I don't have to rewrite my Prolog code.

If you're willing to refactor your native object support in this direction, I think we can get it to a point to merge as well.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub, or mute the thread.

mndrix commented 7 years ago

I've cherry picked the help/apropos and pretty printing commits. I also pushed a follow up commit to fix test failures related to pretty printing.

It'd be nice if help(printf/1) worked so that users don't have to wrap the predicate indicator in a string. I figured that was a minor UI issue at this point, so I didn't want it to hold back the changes entirely.

I didn't actually represent Go structs like person(name("Bob"), age(27)), it was person([name("Bob"), age(27)])

Sorry. I missed the list wrapper during my review.

do you think it might be still better to have fields as a list rather than tuple?

I prefer to work with plain compound terms in Prolog, but SWI-Prolog uses option lists in some places. At this point, I'm not sure we know enough about how Golog users prefer to translate their Go types into Prolog values. Without that kind of feedback, I'm hesitant to merge anything yet.

wvxvw commented 7 years ago

Sorry to re-open this thread: I've went on to try to add predicates to map Go methods to Prolog, and there are several conceptual problems with that. I just wanted to leave these findings here for if this discussion will surface again later:


A little more info on generating methods: it is hard to know if the method is a mutator, and if so, if it should instantiate the variables in the arguments. Suppose this example:

X = x([a(A)]), x_test(X).

Now, suppose the Go code was this:

type X struct {
    A int
}
func (x *X) Test() {
    x.A = 42
}

Or, maybe slightly different:

type X struct {
    A int
}
func (x *X) Test() {
   fmt.Printf("calling Test")
}

Thus, without knowing whether the Go code modified X, we cannot tell if in Prolog code we need to instantiate A... Or, maybe we could... but this would require of us to maintain some sort of mapping between Prolog variables and corresponding Go values, and then verifying if any of them changed after calling Go method... which is becoming very tricky...