Sharing term expansion across modules

Hello,

I have asked this question before [1], but the suggested answer doesn’t seem to work.

I want to create a list of term expansions that can then be imported into, and used within, modules and the “main” user file.

Can this be done?

thank you,

Dan

[1] Term_expansion across several modules - #2 by grossdan

I am using term expansion to demonstrate a high-level domain specific language (DSL). Once expanded terms are processed.

Its somewhat similar to the macro expansions described in the O’Keefe book (p313).

While the DSL is intended for a user file, i noticed that the DSL can be quite useful for modules I developed as well to simplify internal code.

But, i then ran into the problem that in modules DSL keywords don’t get expanded, unless i replicate the term expansion declarations in each module.

I also noticed that if i don’t declare user:term_expansion( … ), the expansions don’t happen in the user file – hence keywords are skipped during processing.

Dan

Thank you.

Yes, i happened to have written the term expansions with user prefix.

I tried all kind of possibilities and it now seems that if i define something like this:

user:term_expansion(keyword1(X,Y), dsl:keyword(X,Y)).

It does expand within a module, but, interestingly only when i consult the module explicitly – when consulting the user file, the expansion in the module doesn’t occur.

Not (yet) sure why.

It does raise in me also the question of the order of expansions – what order would be used when several modules have keywords for expansion and, say, expansion order matters overall.

I am starting to think that using expansions within modules for a dsl is probably not a good idea.

If I understand you correctly, I think the answer is no. You don’t “import” term (or goal) expansions. When a module is loaded, terms in that module will be expanded by looking for term_expansion clauses found in 1) the module, 2) context user, and 3) context system in that order. So there is no “fine grained” control based on imports and exports; the expansion is either local to the module or global (in user or system). Modules can define expansions to be either local or global (as in user:term_expansion(From,To) :- ....

Furthermore, any “global” definitions only apply to code loaded later (expansion happens at compile time). And if you change any term_expansion’s, e.g., by reloading the defining module, it doesn’t redo any of the previous expansions. So it pays to get a clear understanding of how this mechanism works dynamically.

That’s my understanding but I’m a bit of a novice in this area. It would seem to explain what you’re observing, yes/no?

2 Likes

Thank you.

Indeed using the word “import” is a bad choice of term.

Although a module can, apparently, define (“export”) a “global” expansion within the user space.

I guess macro expansions, to declare program wide, across module, DSLs doesn’t really work in Prolog – i guess in languages such as Lisp and Scala – it does work.

Dan

You’ll have to get a little more specific about how you’re using your DSL. Clearly if I define a global expansion and load it first, that expansion will be “program wide”. What you can’t control is what modules/files it will be applied to.

Or you can divide your program in two, i.e., pre and post expansion definition. But this all seems a bit contrived.

I think, per expansion processing rules, a user expansion would (or should) be applied to all modules, as they are consulted. So, the expansion should apply everywhere.

re: specifics

Consider for example a language that allows specifying code blocks such as a “with” statement in VB.NET [1], which enables writing code without need to repeat an object expression – or, to take another example from logtalk – a scope declaration for an object description.

in both cases you want to have block of code, such as so:

begin_block(term_x).
keyword1(a,b,c).
end_block.

Internally, such block could be processed to mean language_term(term1(a,b,c,term_x)).

To get to that, one needs to process expanded terms for begin_block/1 and for end_block and keyword1.

Implicit there is also an ordering if two or more such blocks are declared and when the declaration of a term_y depends on the existence of a prior declaration term_x.

Now, what if term_x would be declared in a module and term_y in another module or user file – the problem then is to ensure correct ordering of expansion, as they occur during consult – which, i guess, is a level of control not specified in prolog – although, it might implicitly be based on some explicit or implicit module import dependency graph.

Dan

Edit:

But, if a user scoped term expansion is declared in a module – then knowledge of its existence seems to happen only after the module is consulted – so, ordering of module consult would, in such a case, affect expansion processing – as you have indicated …

So the programmer should ensure that a module that declares a DSL that should be usable across to program is consulted as early as possible – hence, how :-use_module are declared relative to the user file would be crucial.

[1] With...End With Statement - Visual Basic | Microsoft Learn

Pretty much correct. If you want to use term expansion across modules you have some options. One is to define them in user and use e.g. prolog_load_context/2 to find the module or other relevant context to decide whether to expand or not. For example, this general rule can check that your DSL module was imported into the current module. The alternative is define the rules locally in each module where you want the expansion to happen. You can define the rules in some module and than merely add a link clause to make the rules active in a specific module, e.g.

:- use_module(mydsl).
user:term_expansion(In, Out:) :- mydsl_expansion(In, Out).

And you can even hve mydsl define a rule in user that expands importing the module into the two terms above, so it all looks clean.

One day that will probably change. I did a lot of work on a more module friendly new design, learning from how other systems did a better job, but the project stalled.

Hi Jan,

I tried this and it seems to work well.

Interestingly, i have a goal_expansion in the DSL module and the module has no interface definition – since these require actually implemented goals/predicates.

Yet, when called from outside the module, the call code gets expanded … its nice …

Now i probably want to have two versions of the module – one expanded and for speed and one for debugging to keep the call …

Perhaps this can be achieved through expansions that either include or don’t include expansion code :slight_smile:

Dan

good to know.

But, the way its now, without expansion there is no body …

So, what i need to do is to compile-conditionally include the unexpanded goal as well.

I have something like this:

:- module(my_expansions,[
	
	]).

	goal_expansion(new_item_a(Item), 
			(	
 				gen_id(item_a, Item),                                           % generates new Item id, if Item is var
				module_b:new_item(Item, flags(a,b,c))
			)).

Now in the client code i write:

:- module(my_client, [ call_a/1 ]).

   call_a(Item) :-
           my_expansions:new_item_a(Item).

Once compiled the whole my_expansions:new_item_a(Item) gets replaced by the expanded goal, as so:


   call_a(Item) :-
 		my_expansion:gen_id(item_a, Item),                              
		module_b:new_item(Item, flags(a,b,c))


The reason i am doing this is because i want to have modularity on one hand, while not incur the cost of many! indirect calls, in particular the internal calls to module_b:new_item should be called without indirection.

Dan

Well, unexpanded it should simply be an exported goal/predicate instead:

   new_item_a(Item) :-
		gen_id(item_a, Item),                                           % generates new Item id, if Item is var
     	        module_b:new_item(Item, flags(a,b,c)),

So, during debug the caller reads – to the human eye — new_item_a(Param_a), and the “expansion” code gets called as usual.

I tried the goal expansion i posted, and it works …so i was building on that …

I am not sure i fully understand the code you posted – let me get my head around it …

Edit:

With your example its clearer to me – that’s great – that’s exactly what i want …

Thanks.

So, if this is defined in a module with double/2 exported – the client code will either syntactically have the double call there for the compiler to process – if the expansion failed – or, get that call goal-expanded away …

Yes, that was the missing piece …

thanks

Dan

Yes, i noticed that – i would love an inline directive but i don’t see how it could alleviate the duplication of code. Also, i want to avoid indirections for the purpose of inline code reuse.

Yes exactly – thats the core issue here …

I want to reuse code but i don’t want to incur the call cost, packaging reusable code into a separate goal/predicate and, at the same time, i want to have expansion conditional – and i don’t want to repeat expansions across modules.

I guess lisps macro expansion is a bit more capable.

I think this is great because essentially such one liners are there for information hiding – so you can change things further down the architectural layer - but, the indirect calls do add up. So, best if the compiler can take care of this and based on whether its a debug run or not.

thats interesting, surely worth exploring further

where do you put the inline(F/A) … do i simply put it into module that expands …

can you show how this is used.

I think i get it now. Yes, its simply put in the module and exported and then i can call this as a directive and the assert is called during compile leading to the goal expansion to succeed.

Edit:

I guess, i could place it in some kind of util module and whether its needed, its included