ichiban / prolog

The only reasonable scripting engine for Go.
MIT License
564 stars 27 forks source link

Make Charlist public #315

Closed flashpixx closed 3 months ago

flashpixx commented 3 months ago

Hello,

is it possible to make charList here public to CharList, so it is possible to deal with that type? So it is easy to deal with strings without using :- set_prolog_flag(double_quotes, atom).

Thanks a lot

triska commented 3 months ago

Only a small related note: Lists of characters are usually more preferable to represent strings, because they allow partially instantiated information and convenient reasoning about strings with DCGs. Is there any specific reason you want strings represented as atoms instead?

flashpixx commented 3 months ago

Yes in my case I need atoms and strings (double quoted) differently, I have got strings which containing data and should not be used as atoms, so pure data definition and I have got atoms which are not quoted, which presents the logical fact. In my use-case I use a lot of Go functions which are corresponding with external libraries to add new facts into the Prolog engine and as well some atoms. I need a more strict definition is it a value or a logical fact.

ichiban commented 3 months ago

charList is an implementation detail and I don't think it's particularly useful to be exposed.

If you want to add a fact with an atom and a string to your Prolog interpreter, you can use atom_chars/2:

package main

import (
    "fmt"

    "github.com/ichiban/prolog"
)

func main() {
    p := prolog.New(nil, nil)
    if err := p.QuerySolution(`(Foo, Bar) = (?, ?), atom_chars(FooAtom, Foo), assertz(p(FooAtom, Bar)).`, "foo", "bar").Err(); err != nil {
        panic(err)
    }
    if err := p.QuerySolution(`p(foo, "bar").`).Err(); err != nil {
        panic(err)
    }
    fmt.Println("SUCCESS")
}
SUCCESS

Program exited.

https://go.dev/play/p/oFMLKwL0Ou_K

flashpixx commented 3 months ago

In my use-case I'm add a Go function to the interpreter and have got this function signature: func(en *engine.VM, argCidr engine.Term, k engine.Cont, env *engine.Env) *engine.Promise

the argCidr is a double-quoted string, so how can I get it as Go-String? The debugger shows me as charList and this could be easy casted into the string type

I have create a minimal example to show the issue

package main

import (
    "fmt"

    "github.com/ichiban/prolog"
    "github.com/ichiban/prolog/engine"
)

func main() {
    p := new(prolog.Interpreter)

    p.Register1(engine.NewAtom("foobar"), func(_ *engine.VM, url engine.Term, k engine.Cont, env *engine.Env) *engine.Promise {
        u, ok := env.Resolve(url).(engine.Atom)
        if !ok {
            fmt.Println("string cast fails")
            return engine.Error(engine.TypeError(engine.NewAtom("atom"), url, env))
        }

        fmt.Println(u.String())
        return k(env)
    })

    sols, err := p.Query("foobar(?).", "String Test Case")
    if err != nil {
        panic(err)
    }
    defer func() {
        if err := sols.Close(); err != nil {
            panic(err)
        }
    }()
ichiban commented 3 months ago

As far as I know, a Prolog string is just a list of single-character atoms.

You can't expect it to be charList all the time. It's just one internal representation a string can take. For example, S in S = [a, b, X], X = c. is a valid Prolog string but we can't represent it as type charList string.

Since it's a list of atoms, you can use engine.ListIterator to iterate over it and append each atom to a strings.Builder.

package main

import (
    "fmt"
    "strings"

    "github.com/ichiban/prolog"
    "github.com/ichiban/prolog/engine"
)

func main() {
    p := new(prolog.Interpreter)
    p.Register1(engine.NewAtom("foobar"), func(_ *engine.VM, url engine.Term, k engine.Cont, env *engine.Env) *engine.Promise {
        var b strings.Builder
        iter := engine.ListIterator{List: url, Env: env}
        for iter.Next() {
            switch r := env.Resolve(iter.Current()).(type) {
            case engine.Variable:
                return engine.Error(engine.InstantiationError(env))
            case engine.Atom:
                if len([]rune(r.String())) != 1 {
                    return engine.Error(engine.TypeError(engine.NewAtom("character"), r, env))
                }
                _, _ = b.WriteString(r.String())
            default:
                return engine.Error(engine.TypeError(engine.NewAtom("character"), r, env))
            }
        }
        if err := iter.Err(); err != nil {
            return engine.Error(err)
        }

        fmt.Println(b.String())
        return k(env)
    })

    if err := p.QuerySolution("foobar(?).", "String Test Case").Err(); err != nil {
        panic(err)
    }
}
String Test Case

Program exited.

https://go.dev/play/p/Du8ELNXpwx_v

Or, if you go with atoms, you can use `atom_chars/2` to convert. ```go package main import ( "fmt" "github.com/ichiban/prolog" "github.com/ichiban/prolog/engine" ) func main() { p := new(prolog.Interpreter) p.Register3(engine.NewAtom("op"), engine.Op) p.Register2(engine.NewAtom("atom_chars"), engine.AtomChars) p.Register1(engine.NewAtom("foobar"), func(_ *engine.VM, url engine.Term, k engine.Cont, env *engine.Env) *engine.Promise { u, ok := env.Resolve(url).(engine.Atom) if !ok { fmt.Println("string cast fails") return engine.Error(engine.TypeError(engine.NewAtom("atom"), url, env)) } fmt.Println(u.String()) return k(env) }) if err := p.Exec(`:-(op(1000, xfy, ',')).`); err != nil { panic(err) } if err := p.QuerySolution("atom_chars(Atom, ?), foobar(Atom).", "String Test Case").Err(); err != nil { panic(err) } } ``` ``` String Test Case Program exited. ``` https://go.dev/play/p/rEaDoo57Sft
flashpixx commented 3 months ago

this is the reason to make the charList public, because on an integer, it is quite easy to get it with u, ok := env.Resolve(url).(engine.Integer), so it would be nice I can do it with u, ok := env.Resolve(url).(engine.CharList), so it reduces the code complexity and it is more consistant. In general it would be nice to do u, ok := env.Resolve(url).(engine.String)

ichiban commented 3 months ago

I feel the same about u, ok := env.Resolve(url).(engine.String) being a better notation and I wouldn't have missed the opportunity if I could make it so. Unfortunately, this idiom only works for an atomic term which has a single internal representation. Compound terms, on the other hand, have many internal representations.

Currently, a Prolog string can be an Atom, compound, list, partial, charList, or combination of them. This is because a Prolog string is just a list of one-char atoms.

Maybe a Prolog string isn't suitable for your use case. How about storing your Go strings externally and passing around the surrogate keys?

package main

import (
    "fmt"

    "github.com/ichiban/prolog"
    "github.com/ichiban/prolog/engine"
)

func main() {
    urlRepository := map[int]string{
        123: "String Test Case",
    }

    p := new(prolog.Interpreter)
    p.Register1(engine.NewAtom("foobar"), func(_ *engine.VM, urlID engine.Term, k engine.Cont, env *engine.Env) *engine.Promise {
        var s string
        switch id := env.Resolve(urlID).(type) {
        case engine.Variable:
            return engine.Error(engine.InstantiationError(env))
        case engine.Integer:
            s = urlRepository[int(id)]
        default:
            return engine.Error(engine.TypeError(engine.NewAtom("integer"), id, env))
        }

        fmt.Println(s)
        return k(env)
    })

    if err := p.QuerySolution("foobar(?).", 123).Err(); err != nil {
        panic(err)
    }
}
String Test Case

Program exited.

https://go.dev/play/p/tJ9zT9ljyjN

flashpixx commented 3 months ago

I feel the same about u, ok := env.Resolve(url).(engine.String) being a better notation and I wouldn't have missed the opportunity if I could make it so. Unfortunately, this idiom only works for an atomic term which has a single internal representation. Compound terms, on the other hand, have many internal representations.

It would be really nice if you can add the engine.String as enhancement, I have written a function which converts me the atom into my Go string, so this is dirty fix at the moment which works

ichiban commented 3 months ago

engine.String won't happen in the foreseeable future. Again, this is because a Prolog string is much much complex than a Go string. You need to either accept the complexity by treating it as a list or avoid the complexity by falling back to atoms or surrogate keys.

A simple utility might help you to mitigate the complexity:

package main

import (
    "fmt"
    "io"

    "github.com/ichiban/prolog"
    "github.com/ichiban/prolog/engine"
)

func main() {
    p := new(prolog.Interpreter)
    p.Register1(engine.NewAtom("foobar"), func(_ *engine.VM, url engine.Term, k engine.Cont, env *engine.Env) *engine.Promise {
        r := NewCharListReader(url, env)
        b, err := io.ReadAll(r)
        if err != nil {
            return engine.Error(err)
        }
        fmt.Println(string(b))
        return k(env)
    })

    if err := p.QuerySolution("foobar(?).", "String Test Case").Err(); err != nil {
        panic(err)
    }
}

type CharListReader struct {
    iter engine.ListIterator
}

func NewCharListReader(t engine.Term, env *engine.Env) *CharListReader {
    return &CharListReader{
        iter: engine.ListIterator{
            List: t,
            Env:  env,
        },
    }
}

func (c *CharListReader) Read(p []byte) (int, error) {
    iter := &c.iter
    if !iter.Next() {
        if err := iter.Err(); err != nil {
            return 0, err
        }
        return 0, io.EOF
    }

    switch char := iter.Env.Resolve(iter.Current()).(type) {
    case engine.Variable:
        return 0, engine.InstantiationError(iter.Env)
    case engine.Atom:
        s := char.String()
        if len([]rune(s)) != 1 {
            return 0, engine.TypeError(engine.NewAtom("character"), char, iter.Env)
        }
        b := []byte(s)
        copy(p, b)
        return len(b), nil
    default:
        return 0, engine.TypeError(engine.NewAtom("character"), char, iter.Env)
    }
}
flashpixx commented 3 months ago

Thanks, I came to a very similar solution