How to define an exportable term_expansion/2?

Is it possible to define a module that exports term_expansion/2.

When I tried to do this:

:- module(my_expansion, [term_expansion/2]).

:-use_module(my_expansion) produced this warning:

Warning: Local definition of user:term_expansion/2 overrides weak import from my_expansion

One work-around would be to define:

:- module(my_expansion, [my_term_expansion/2]).

and then use it by:

:- use_module(my_expansion).

term_expansion(Clause, Expansion) :- my_expansion(Clause, Expansion).

but that’s extra boilerplate.

Or, I could define user:term_expansion/2 in module my_expansion, but that’s frowned upon, according to the documentation for term_expansion/2:

If there is no local definition, or the local definition fails to translate the term, expand_term/2 will try term_expansion/2 in module user . For compatibility with SICStus and Quintus Prolog, this feature should not be used.

term_expansion/2 is a hook so it would not be exported, no?

You can define user:term_expansion/2, so even though it’s not exported, it does take effect. But apparently that’s frowned upon (even though it’s used in some code that I’m maintaining and trying to modernise a bit, namely pack(edcg)).

When I have doubts about the documentation I just go with what the SWI-Prolog code does. When there are several variations I first look to see if the way it is done has changed over time, test cases are a prime example of this. However for expand.pl I see both user:term_expansion and system:term_expansion but I also see expand_arithmetic/2 for use with is/2 and for which I personally consider a DSL within Prolog.

However there are specific term_expansion rules for some of the dialects like IF/Prolog, using ifprolog_term_expansion/2 but then other dialects just use system:term_expansions/2.

If I were writing code I would go with what you have done unless Jan notes a reason I am not aware.


pack(edcg) - still love the package and glad you introduced me to it. The only problem I have with it, that I think I noted before, is that the rules are very specific to what was needed for the Aquarius compiler. I know one can add more rules but I only figured that out after a very hard few days.

Interesting side note. Peter Van Roy was mentioned in the CS SE answer. :slightly_smiling_face:

The code I’m trying to modernise was written 7 years ago, based on code that’s over 30 years old. I suspect that SWI-Prolog has changed a bit since then.

As for other dialects – it’s nice to maintain compatibility, but that’s not my primary aim (maybe it should be?) As it is, I’ve changed the code to use SSU (=>), which really breaks compatibility; although reversing that would be quite easy.

I have an example of a more complex accumulator (used for a symbol table) here: https://github.com/kamahen/pykythe/blob/d54fd05096af5eb47efb863f613f12d5d80c6c41/pykythe/pykythe.pl#L271
and https://github.com/kamahen/pykythe/blob/d54fd05096af5eb47efb863f613f12d5d80c6c41/pykythe/pykythe.pl#L3114

The basic idea of EDCG’s accumulators is that you define a predicate that takes the accumulator argument (whatever’s inside the square brackets) with two additional arguments for input and an output. The input and output can be terms – in my symbol table accumulator, the input and output term have two parts (the symbol table and a list of items that weren’t found in the symbol table); the accumulator does a lookup in the symbol table and if that fails adds the item to both the output symbol table and the list of not found items.

A much simpler example of an EDCG accumulator is in edcg/t/synopsis.pl:

edcg:acc_info(adder, X, In, Out, plus(X,In,Out)).

And an example of its use:

increment(I) -->> [I]:adder

expands to

increment(I, A, B) :- plus(I, A, B).

If you have multiple items inside the square brackets, they’re applied one after the other:

foo(X,Y) -->> [X,Y]:adder.

expands to

foo(X, Y, A, B) :-
    plus(X, A, C),
    plus(Y, C, B).

Peter Van Roy did some amazing work, both programming (Aquarius compiler) and writing (Concepts, Techniques, and Models of Computer Programming). Peter Van Roy: Partial Curriculum Vitae
He was at UCB when Berkely Unix was being developed, also early and important work on RISC and RAID.

1 Like

Anyway, getting back to term_expansion/2, it’s generally frowned upon to define predicates in module system (Term_expansion/2 in a module, user or system? - #5 by jan) and the documentation says to avoid defining user:term_expansion/2 (although I don’ know enough about other dialects such as SICStus to know why it should be avoided).

1 Like

BTW, pack(edcg) has a cute trick (presumably invented by Michael Hendricks) to avoid its user:term_expansion/2 being used where it hasn’t been explicitly imported:

:- module(edcg, [op(1200, xfx, '-->>'), edcg_import_sentinel/0]).

edcg_import_sentinel. % dummy predicate for controlling use

user:term_expansion(Term, Expansion) :-
    prolog_load_context(module, Module),
    Module \== edcg,
    predicate_property(Module:edcg_import_sentinel, imported_from(edcg)),
    edcg_term_expansion(Term, Expansion).

edcg_term_expansion((H-->>B), Expansion) :- ...

(This technique can also be used to control user:unify_clause_hook/5)

2 Likes

About 5 years ago, I tried the same thing to avoid the “extra boilerplate”, for example, to use include/1 directive. But I could not find my favorite solution, though “my favorite” is still not defined clearly. Finally I was forced to be satisfied with the boilerplate.

The simple answer is that you can typically not import/export goal/term expansion as they are multifile predicates. The only thing you can do is to define the complicated stuff as a predicate with a different name and then add a clause to term_expansion/2 where you want it that calls the complicated stuff. You could use ciao’s approach where an extension is specified using :- module(Name, Exports, Extensions), and the extensions basically include some code. So, you can define a module that deals with the real code and the extension which loads this module (modules) and uses the include logic to define local rules for term/goal expansion, operators, etc.

SWI-Prolog’s term/goal expansion is still mostly based on the state of the art in the late 80s, with a small enhancement to chain over module inheritance (module → user → system).

I’ve been working on the design of a new system a couple of years ago. There is more modern stuff in notably ECLiPSe, Ciao and Logtalk. Little agreement though and these systems also come with problems. In addition, so much code relies on how it works as it does that a new design must be at least 99% compatible. The project stalled. In part because expansion touches the source layout management and I wanted to solve that first. In part because I think I have more faith in inlining and partial evaluation as a way into the future.

1 Like

It would seem that pack(edcg)'s “trick” for limiting user:term_expansion/2 and prolog_clause:unify_clause_hook/5. to only modules that directly use it is the simplest after all, if I want to minimise boilerplate.

As for replacing term_expansion with inlining – that would be a nice thing (although, in my experience, those kinds of changes can be done with only a few lines of term-expansion); but they don’t handle things like inlining maplist/3 or DCG-like things. For DCG-like term expansion, maybe there could be some helpers, such as for handling control structures and meta-predicates; the existing EDCG code has a lot of very similar clauses that can easily contain a hard-to-detect typo (e.g. handling G1,G2, G1;G2, G1->G2;G3, \+G, call(G) etc.)

Recent GitHub commit of interest:

Thinking out loud in case someone wants to follow up on this as I don’t have the time at present.

In reading the the recent commit there is a section for Predicate visibility. That reminted me of the Linux which command and then remembered that SWI-Prolog has explain/1,2 (Not saying they provide the same functionality, it was what popped into my mind).

So the rhetorical question is: Can explain/1,2 help explain where predicates and operators originate?

explain/1 tells you where predicates are defined and used. With multiple instances of a predicate with the same name/arity living in different modules it does (AFAIK) not make clear which instance is used where. The predicate_property/2 implementation_module(Module) can be used to figure out which module provides the definition if some name/arity is called in some specific module. Surely explain/1 could exploit that. There is some risk that adding that detail makes things more confusing.

The system does not keep track on where operators are defined. You can ask a module which operators it exports and with some effort you can figure out which modules import these operators. Operators defined using a directive (the most common way) are not tracked. Possibly we should do so. This requires something like op_property/2. There is some room for more properties. I’ve also been thinking about a property that defines that an operator wants to be separated by a space from its arguments regardless of whether this is required to separate the input tokens.

1 Like