LogtalkDotOrg / logtalk3

Logtalk - declarative object-oriented logic programming language
https://logtalk.org
Apache License 2.0
425 stars 31 forks source link

Using term_expansion to produce multiple objects fails on predicates #208

Closed unthingable closed 6 months ago

unthingable commented 6 months ago

Not sure if I'm misusing it, but seems like this should be possible.

    term_expansion(ab, [
        (:- object(a)),
        (:- public(x/1)),
        x(1),
        (:- end_object),
        (:- object(b)),
        (:- public(y/1)),
        y(1),
        (:- end_object)
    ]).

When loading a file with ab. term using this hook:

!     System error
!       in clause for predicate y/1

From limited poking around it appears to react poorly specifically to predicates declared in the second object. For example, this works fine:

    term_expansion(ab, [
        (:- object(a)),
        (:- public(x/1)),
        x(1),
        (:- end_object),
        (:- object(b)),
        (:- dynamic),   % just to see if second object can handle directives
        (:- end_object)
    ]).
...

?- {loader}.
...
% (0 warnings)
true.

?- a::x(X).
X = 1.

?- object_property(b,X).
X = context_switching_calls ;
X = source_data ;
X = (dynamic) ;

Logtalk 3.78.0 SWI-Prolog (threaded, 64 bits, version 9.3.2-10-gf146a507d)

pmoura commented 6 months ago

Fixed in https://github.com/LogtalkDotOrg/logtalk3/commit/3b417b243fadb3abb7d36a4ce72431091d4c8a09:

$ cat te.pl
term_expansion(ab, [
        (:- object(a)),
        (:- public(x/1)),
        x(1),
        (:- end_object),
        (:- object(b)),
        (:- public(y/1)),
        y(1),
        (:- end_object)
    ]).

$ cat ab.lgt
ab.

$ swilgt -q
?- [te].
true.

?- logtalk_load(ab, [hook(user)]).
true.

?- a::x(X).
X = 1.

?- b::y(Y).
Y = 1.

Can you tell me your first and last name so that I can credit the bug report? Fine if you prefer to remain anonymous.

unthingable commented 6 months ago

Thank you!

FWIW, this is what I needed it for:

:- object(opt, imports(options)).
:- end_object.

:- protocol(defaultp).
    :- public(default/1).
    :- mode(default(-term), one).
:- end_protocol.

:- object(types_util).
    :- public(replace_nth_arg/4).
    replace_nth_arg(N, NewArg, Pred0, Pred) :-
        Pred0 =.. [P|List],
        list::nth1(N, List, _, TempList),
        list::nth1(N, NewList, NewArg, TempList),
        Pred =.. [P|NewList].
:- end_object.

:- object(types_hook, implements(expanding)).
    accessor(Name, Num, Obj) :-
        atom_concat(Name, '_set', SetterName),
        atom_concat(Name, '_get', GetterName),
        Getter0 =.. [Name, X],
        Getter =.. [GetterName, X],
        Setter =.. [SetterName, Val, Pred],
        Obj = [
            (:- public(Name/1)),
            (Getter0 :- parameter(Num, X)),
            (:- public(GetterName/1)),
            (Getter :- parameter(Num, X)),
            (:- public(SetterName/2)),
            (
                Setter :- this(Pred0),
                types_util::replace_nth_arg(Num, Val, Pred0, Pred)
            )
        ].

    term_expansion(type(X), Obj) :-
        term_expansion(type(X, []), Obj).

    term_expansion(type(X, Options), Obj) :-
        X =.. [Atom|Args],
        length(Args, Length),
        {numlist(1, Length, Numbers)},
        length(Vars, Length),
        Odecl =.. [Atom|Vars],

        meta::map(accessor, Args, Numbers, Accessors0),
        list::append(Accessors0, Accessors),

        (   opt::option(default(Def), Options)
        ->  Companion = [
                (:- object(Atom, implements(defaultp))),
                default(Def),
                (:- end_object)
            ],
            Head = object(Odecl, extends(Atom)),
            DefPred = [default(Def)]
        ;   Companion = [],
            Head = object(Odecl),
            DefPred = [] 
        ),

        list::append([
            Companion,
            [(:- Head)],
            Accessors,
            DefPred,
            [(:- end_object)]
        ], Obj).

:- end_object.

It's the record I always wanted in Prolog (with a Scala case class smell).

type(
    foo(a,b),
    [default(foo(1,2))]
).

?- foo(3,4)::b(X).
X = 4.

?- foo(3,4)::b_set(999, X), X::b(Y).
X = foo(3, 999),
Y = 999.

?- foo(_,_)::default(X).
X = foo(1, 2).

?- foo::default(X).
X = foo(1, 2).

The companion object simplifies access to "static" methods.

Perhaps there is a better and easier way to do this, I'm quite new to Logtalk.

pmoura commented 6 months ago

Nice use of the term-expansion mechanism.

When working with parametric objects, you can always derive from them other objects that fix some of the parameters. For example:

:- object(foo(_X_, _Y_)).

    :- public(x/1).
    x(_X_).

    :- public(y/1).
    y(_Y_).

    ...

:- end_object.

:- object(foo, extends(foo(1,2))).

:- end_object.

P.S. You can use integer::sequence/3 instead of the backend numlist/3. There's also list::length/2 for portability.

unthingable commented 6 months ago

Excellent suggestions, thanks!

Since we're on this topic, a couple of unrelated questions.

  1. In your example, how would you get the term foo(1,2) from the object foo?
  2. Is there a property chaining operator? E.g. where instead of A::a_get(B), B::b_set(X, C), C::c_get(D) one could write A:>a_get:>b_set(X):>c_get(D) (using an imaginary operator :> that chains members on the last argument of previous one).

Pretty sure I can define a naive version of one, just wanted to make sure one doesn't already exist. That would make an almost complete lens.

What would you say about this approach?

pmoura commented 6 months ago

For (1), a solution could be:

:- object(foo(_X_, _Y_)).

    :- public(x/1).
    x(_X_).

    :- public(y/1).
    y(_Y_).

    :- public(default/1).
    default(This) :-
        this(This).

    ...

:- end_object.

:- object(foo, extends(foo(1,2))).

:- end_object.
?- foo::default(D).
D = foo(1, 2).

For (2), there's currently no chaining operator. I suggest you look into DCGs.

pmoura commented 6 months ago

Closing as the original bug reported is fixed.