Dicts, attribute variables and cyclic terms

Some discussions are going on in the PIP working group related to synchronize (as a lightweight standardize) some aspects between Prolog systems. I’d like to seek opinions on a few things. Let me start with the relevant proposals and how I see this wrt. SWI-Prolog.

dicts/named arguments

ECLiPSe uses p{x:1, y:2} for what it calls struct, and Ciao calls this named arguments with a slightly different syntax. Either way, the idea is that we declare the argument names of a compound term, say :- struct p(x,y,z), after which a term p{k:v, ...} creates a compound term p(X,Y,Z) where every named argument is placed in the corresponding argument and other arguments of the compound are unbound. So, p{y:2} maps to p(_,2,_). The interpretation should ideally be handled by read/1.

This of course conflicts with SWI-Prolog’s dicts that use fully dynamic sets of keys. Now I have the impression that the “tag” (as in tag{}) is not used for much. People tend to use _ (anonymous variable). This matches anything, which you typically want. The drawback though is that makes the dict non-ground, while this is typically the intend. I started using #{...}, which is fine as long as you use it consistently.

We are moving towards a consensus that #{...} is a dynamic dict as SWI-Prolog uses them, the use of variables as tags is not allowed and any other atom refers to an argnames/struct as outlined above. I’m tempted to agree with that. I’d probably allow for any other atom to map to a current dynamic dict as long as there is no declaration for the argument names, with a flag to control this.

Attributed variable syntax

Somewhat related, ECLiPSe allows writing variables with attributes as Var{a1:v1, ...}. SWI-Prolog has what I always considered an obsolete option to write terms with attributed variables using this syntax, but reading would (now) produces a dict rather than a variable with attributes. SICStus inspired copy_term/3 is the way the Prolog toplevel handles constraints. If we stop allowing for _{k:v, ...} as dict syntax, we free up this syntax.

Cyclic term syntax

Stronger related, Joachim from ECLiPSe proposes to handle the result of e.g., X = f(X) as X{= : f(X)}. This generally allows to bind a term as you read it to a variable. I guess this also means you can write Y = f(X{= : p(1), X) which would be the same as X = p(1), Y = f(X,X).

This as opposed to what SWI-Prolog copied from SICStus, writing the result of X = f(X) as @(X, [X=f(X)]), i.e. a cyclic term is written as a term @(Skeleton,ListOfUnifications). The disadvantage is twofold. First, to get the real thing you need to rebind the unifications and second, terms @(X,Y) are ambiguous.

Note that @dmchurch has proposed (Inline subterm naming syntax (was: Syntax for unify-and-remember in head?)) to use (Var) term to mean the same as the proposal here Var{= : term}. (Var) term looks prettier, but I’m a little afraid of the ambiguities. Surely if term is a postfix operator, this is already valid syntax, but with different interpretation.

Opinions?

Notably, is this a good plan and if so, how should we deal with this to comply without introducing a big backward compatibility issue?

2 Likes

My [revised] “automatic” thoughts:

  • The syntax {<key>:<any>,...} for “dicts”
    (after all, do we need e.g. named lists?);
  • The syntax <atom>{<key>:<any>,...} for “structs”
    (i.e. where the name is the functor name);
  • The syntax <var>{<key>:<any>,...} for attributed vars.

To complement Jan’s message, for context it may be useful to read the corresponding PIPs, PIP-0102 Dictionaries in Prolog (dynamic) and PIP-0104 Terms with named arguments (static dicts).

That would be nice, but Prolog defines the general syntax {Term}, which is syntactic sugar for {}(Term) that is used originally for DCG escape to Prolog. Since then, it has been used by many libraries for all sorts of roles. I’m afraid the impact would be too large.

OK, I see: but then, just like {} is (kind of) a reserved name, I’d like your idea of reserving #, as in #{<key>:<any>,...}: sort of “the dict struct”.

(Of course, I am not so much vouching for a specific name, it’s more about clean and uniform.)

I agree with that because it can also accidentially happen that you produce a nonreadable term by writing:.

?- A=B{}, B=[].
A = []{}, %nonreadable
B = [].

1 Like

Press Ctrl-E or use 3 backticks to enter “preformatted text” (e.g. a code block) in Discourse.

1 Like

I’m sorry to report that I use this extensively :slight_smile:
I am using tagged dict as a basic OOP where I use user function on dict as dynamic method dispatch.
I have to say that this works really well and allows for an easy way to use functional syntax in prolog.

I used this in multiple libraries:

  • units with q{} and qp{} dicts
  • a work in progress library for strided array with array{}. this one is particularly important for me as it allows to imitate numpy api almost perfectly.

Of course, one could argue that such constructs would be better (or more efficient) with what you call struct (or record).
But when designing a new library and api, it is extremely convenient to avoid any sort of forward declaration (as it is one more thing to care about. This is the reason I don’t use edcg) and to be able to add a new key:value dynamically.

I suppose this would cover my needs, where I would use a fully dynamic dict when designing the library and move later to a struct by adding a declaration ?

If we move to tagged dict to static struct, would it be possible to retain functional notation on those ?

I think a comparable subject is the Python object system, which is based on Python dict.
One could argue that the whole success of Python is because python object are so dynamic and convenient.
Another point of view is that this single design choice is what made python so slow for so many years.

Just a note, the default dict json parser uses vars as tag by default.
Changing that may break existing code ?

Not sure. It is definitely not part of the current PIP proposal. It may be possible as the (one) function in SWI-Prolog is .(Obj,Func). It might be expensive though as it requires runtime information on the argument names.

Sure. It seems we can agree on the target. We need to find a way to make the transition bearable. The problem banning _{...} as a dict is that we can no longer do

 _{k1:V1, k2:V2} :< Dict

If we only use # for dynamic dicts we can use #{k1:V1, k2:V2} :< Dict, but if, like you, we still want to use functions on typed dynamic dicts, this does not work. Possibly we can decide that the tag is ignored? Or that # matches any tag? So,

#{x:X} :< point{x:10,y:20) 

is true, but

velocity{x:X} :< point{x:10,y:20) 

is false?

To be a bit more (not 100%) compatible with the current dict implementation it would make sense to turn A{} into #{} if A is a free variable. And if A is not free or not an atom then it should raise an exception.

Maybe the best would be to have a module wide option as a last resort to enable the old behaviour.

But then the question is how to handle dict at module boundaries. It sounds complicated but maybe it isn’t.

Ok, some more opinion polling. I think we can summarize that there are no strong objections to these three conclusions (if, so, please stand up).

  • #{...} is a dynamic dict
  • <atom>{...} maps to a compound if there is a matching argnames/struct declaration.
  • _{...} should be phased out as dict representation.

This leaves undefined as to what happens with <atom>{...} if atom has no argnames/struct declaration. I think this should be controlled by a module-specific flag (inherited over the module system, just like operators and similar syntactic settings). For example

  • Flag unknown_tag with values error and dict. Possibly also warning, printing a warning and creating a dict?

Next, what to do with _{...}. Just ditching as dict representation is not an option, so I think we need another flag

  • Flag var_tag with values attvar, # or dict. dict would retain the current behavior. attvar creates an attributed variable (also some form of key-value set :slight_smile: ) and # creates a dict #{...}.

In addition, in #{...} :< <atom>{...} and #{...} >:< <atom>{...} and <atom>{...} >:< #{...} are considered to have matching tags. What about <atom>{...} :< #{...}? It can be a useful way to extract a typed dict from an anonymous dict.

Questions

  • Do you have comments on the above?
  • What should go into the next stable release? There is pressure to release this quite soon because the current stable no longer builds using various modern C compilers. Getting this all resolved probably takes too much time. Should we merely add the two flags (or what they will be after comments) using the current defaults?
2 Likes

Any unintended ripple down effects on the underlying predicates, e.g., dict_create/3, given various flag values? (I’m assuming default flag values would ensure maximum backwards compatibility.)

Also wondering whether existing unification semantics is maintained, e.g.,

?- tag{x:X,y:Y} = T{x:V1,y:V2}.
X = V1,
Y = V2,
T = tag.

?- T{x:V1,y:V2} = tag{x:X,y:Y}.
T = tag,
V1 = X,
V2 = Y.

would it be possible to make this module aware ?
so that it won’t leak everywhere if present ?

Would it be possible to make the default dict ? for backward compatibility ?

I’m all for having a stable release as soon as possible, so that we can use ==> SSU DCG and attributed variables in tabled goals :slight_smile:

In general, I don’t know. For dict_create/3 I guess using an unbound tag should be handles in the same spirit as _{...}?

I think that will remain working as is, but the flag values may make it impossible to create T{...}. Possibly a bit confusing is that while ?- #{x:X} :< pt{x:1} works with X=1, ?- #{x:X} = pt{x:1} can only fail. Same for ==/2. I fear that is the price we need to pay for backward compatibility and named dynamic dicts.

The PIP proposal consensus is that the scope of argname/struct declarations follows the same rules as operators. So, yes.

Initially, surely. The question is what needs to happen on the long run? One option is to add a declaration that some that denotes a dynamic dict.

Me too and I think we are getting close. The question is whether or not we should sneak in some of this stuff to make it easier to write code that runs on the next stable and next devel series. I’m tempted to add # as tag that matches any tag in (:<)/2 and (>:<)/2. That would resolve (at least in my current coding style) most of the need for using _{...}. Of course we could do so using a flag, but I’m tempted not to use a flag here. It seems pretty unlikely this change will break anything. Objections?

Next step could be to migrate all libraries to use #{...} unless they actually use the tag? That would give some experience in the impact.

In my own code I used

:- struct f(a,b,c).
... Y = #f(a: 3, c:X).

converting to

... Y = f(3,_,X)

implemented by goal_expansion.

I have no objection to replacing #f( by f{ .

Your struct will in due time be handled by SWI-Prolog itself, most likely by read/1. Possibly with a different name.

In the mean while, all will keep working as is. I’m finalizing some preparation steps that should put us on a path to adopt the PIP proposals. That mostly implies getting rid of _{,,,,} as anonymous dict, replacing it with #{...}. The next release (and the next stable) will provide the support for using #{...} wherever _{,,,,} is used now and deprecate _{,,,,}.

Unlike the PIP, we will allow other atom tags to be used for dynamic dicts, possibly (only) with a declaration.

Btw. I would like to have the possibility to temporarily disable the dict dot check in the reader so that I were able to reimplement the dot by myself for that scope. Then I could write:

with_dict( D, TERM).

and in the TERM I could replace D.key terms with its values in a way that don’t result in struggle with nested bagof and lambda calls.

read/1 does nothing with the dot terms. They are handled in one of the last steps of the system term expansion. So, if you want these terms to do something different, use term or goal expansion to replace them with something else.

I’m sorry that was a guess from me. I think I found it.

(ins)?- term_to_atom( T, 'writeln(a.b)'), expand_goal( T, X).
T = writeln(a.b),
X = ('.'(a, b, _A), writeln(_A)).

and

(ins)?- listing( '.').
'$dicts':'.'(Data, Func, Value) :-
...

from boot/dicts.pl

are working together here.

But I didn’t find out where I can find the corresponding goal_expansion rule.

That rule, if it exists, should then be temporarily disabled.