Finding the source code name for a variable at run time

I want to write a predicate replaceVariables/2 the replaces the variable names in a string with the current value of those variables like this:

?- X = hello, replaceVariables(X, "The value is [X]", Output), writeln(Output).
The value is hello

Assuming I have a way to parse the string into useful parts, it appears as though read_term_from_atom/3 will allow me to convert the "X" part of the string in the second argument of replaceVariables/2 into a name+assigned variable like this:

[debug]  ?- read_term_from_atom("X", Term, [variable_names(Vars)]).
Vars = [X= $VAR(Term)].

All Iā€™m lacking is a way to know that X is the name of the first argument of replaceVariables/2 at run time so that I can match them up.

Otherwise, Iā€™ll have to do something like passing in a pair for each variable, which just seems odd, like this:

?- X = hello, replaceVariables('X'-X, "The value is [X]", Output), writeln(Output).
The value is hello

I realize that the format predicates do something along these lines, but they require matching of order and my strings could have the same variable in multiple places, the order may change over time, etc. Iā€™d like to have a predicate that separates name from order to prevent inadvertent bugs.

Any ideas?

(BTW: what I really want is to not have to pass the variables in at all like this:

?- X = hello, replaceVariables("The value is [X]", Output), writeln(Output).
The value is hello

Which is why I asked this question. I was hoping to be able to rewrite replaceVariables/2 at load time with inline predicates using the variable names from the string to solve the same problem)

1 Like

This looks terribly like you are re-implementing format/2, so I must be missing something important here.

?- format("The value is ~w", [hello]).
The value is hello
true.

You can use format/3 to write to many other things, not just standard output.

So, what are you trying to do?

I am trying to write a predicate that will be used by users to output long strings of templated text in an interactive fiction like engine. Hereā€™s a (slightly modified) example from one of the more well known interactive fiction titles ā€œCounterfeit Monkeyā€. It is the ideal version of what Iā€™d like to make work:

\+ canMove(Direction)
->
   outputText("You can't go [Direction]: [b]South[/b] and [b]east[/b] 
   lead to more of the park; there is a small [cinemaExterior]
   [if(notHandled(backpack))](where I left my pack)[endIf] to the 
   [b]north[/b], and the entrance to the [churchExterior] is [b]west[/b].")
;
...

All of the text in [] is treated as commands to the templating engine. Simple atoms (that arenā€™t reserved words like if) are interpreted by the templating engine as ā€œconvert this atom to English and insert it hereā€, and everything else is a command to the template engine (e.g.[if()], [endif], [b]).

In this example, [Direction] is referring to a variable in the code surrounding the string that will contain an atom like idNorth or idWest that needs to be converted to English and filled into the string that gets printed for the user. This is the motivation for the problem Iā€™m trying to solve.

I understand how to solve the rest of the templating engine, I just canā€™t figure out a nice way to get local variables inserted. The approach that format/3 takes makes you match the order and count of variables being replaced which isnā€™t ideal when you have big chunks of text, with potentially repeating variables, that might get moved around as editors play with it over time. It is ripe for introducing bugs.

I think the solution to what you want is in quasi quotations. See also SWI-Prolog -- Manual

Wow! Yeah, I think that is definitely what I want. Iā€™ll have to dig around a bit to make sure but it sure seems like it. Nice!

@jan:Iā€™m trying to read that code to figure out how quasi quotations solves the original problem I had: ā€œhow to find the source code name for a variable at runtimeā€. Meaning: how does quasi map the variable To in test(To) to the textual value "To" in the string?

test(To) :-
    write({|string(To)||
           | Dear {To},
           |
           | I'm happy to announce a string interpolation quasi quoter.
           |}.

Am I understanding correctly that a system hook ā€˜$parse_quasi_quotationsā€™/2 needed to be added to accomplish this? Or am I just missing the magic somewhereā€¦

1 Like

Is read_term/2 with option variable_names what youā€™re looking for?

It does it exactly the way you did in your original post. Where you used read_term_xxx and variables_names option to get a dictionary of name-term values. You mentioned one way of solving your problem was to pass such as structure into your replaceVariables predicate but

but they require matching of order and my strings could have the same variable in multiple places, the order may change over time, etc. Iā€™d like to have a predicate that separates name from order to prevent inadvertent bugs.

however a name-value dictionary doesnā€™t suffer from any of the things you mentioned (name is separated from order as you would lookup by key. e.g. member('X'-V, Dict). So you could split up your template string into parts where you look for '[' , string(Name), ']' and substitute the value (member(Name-V, Dict)). In fact this is what happens with quasi-quotations, they are effectively a DCG where you define how the grammar of the quotation should be parsed, and the resulting parse tree is replaced in the code at compile time.

I maybe misunderstanding, but I donā€™t think so. Letā€™s convert the example to this:

test(To) :-
    magicFunction(To, "Dear {To}").

Is there a way I can use read_term/2 in the implementation of magicFunction/2 such that this happens:

?- test(fred)
Dear fred

Maybe Iā€™m missing something obvious hereā€¦

Ahhh, I think this is where the magic is happening (as far as my question is concerned). In quasi, the token string {|foo(To)||Dear [To]|} gets transformed by the predicate foo to a set of terms at compile time (this is the key, I think) and replaced in the source. Thus the variable To in Dear {To} can be unified with the To in test(To) :- by the Prolog engine when it runs.

So it looks something like this (assuming the foo function just creates an AST that is a list) to the compiler when it is processing the final code:

% This...
test(To) :-
    magicFunction(To, {|foo(To)||Dear [To]|}).

% Transforms to this at compile time:
test(To) :-
    magicFunction(To, ["Dear ", To]).

Which makes implementing magicFunction easy.

Is that what quasi quotations is doing?

Update: Nope.

I tried this and it is not what is happening. If you use term/goal expansion to convert (without using quasi quotations) {|foo(To)||Dear [To]|} to ["Dear ", To], it turns out that the To in the goal expanded version does not end up with the variable name To. It really transforms to something like:

% This...
test(To) :-
    magicFunction(To, {|foo(To)||Dear [To]|}).

% Really transforms to this at compile time (and thus doesn't work):
test(To) :-
    magicFunction(To, ["Dear ", _4567]).

So something else is going on to allow quasi notations to bind the variables within the {| || |} with variables outside of it.

Your first idea about quasi-quotations was correct. Is it so surprising that by doing something different (i.e. term/goal expansion) that you got a different result?

I donā€™t really understand this either. It might clarify that quasi quotation expansion is part of the parser (read_term/3). It is indeed a call to foo/4 providing foo/4 with access to the arguments, their names and the raw text. This call returns a term (or raises a syntax_error exception). There are no limitations to what you can do. One of the tricky things you can do is to generate a dict with a function on it, e.g.

 foo{data:Data, ...}.eval()

That is done by e.g., the string quasi quotation library to concatenate the various pieces of the string.

@jan pointed out where the magic is happening. There are two useful predicates for mapping variable names in source to the actual variables they become after parsing so they can be used in goal or term expansion: var_property/2 and prolog_load_context/2. I ended up using prolog_load_context/2 in the implementation of my templating engine.

For context, I wanted something very much like quasi quotations but I didnā€™t want the user to have to explicitly list the variables used in the quotation since the syntax of the quotation could fish them out automatically:

% Test.pl
% Here's an example rule the user might write which checks if
% there is a found item, and prints it out 
test :- htnMethod(
        if([findItem(X)]),
        do([sayText("I found item [X]")])
    ).

When loaded, the sayText/1 predicate is converted to a sayList/1 predicate which is the result of parsing the string and converting it to something that can be executed when test() is run (with a warning that will be explained further down).

In the next code, notice that findItem(A) and sayCommand(A) both use the same variable (A), which was the original goal I was trying to achieve and am now able to via prolog_load_context/2:

?- consult("Test.pl").
Warning: Test.pl:3:
Warning:    Singleton variables: [X]
true.

?- listing(test).
test :-
    htnMethod(if([findItem(A)]),
              do([sayList([sayString("I found item "), 
                           sayCommand(A)])])).

true.

Here is the full code for this example:

% TermExpansion.pl
goal_expansion(htnMethod(if(IfList), do(DoList)), Replacement) :-
    % For performance reasons: do a quick check to see if 
    % any conversion is needed
    nonvar(DoList),
    once((
        member(X, DoList),
        nonvar(X),
        X = sayText(_)
     )),
    !,
    % Get the source code variable names and values from the source
    % to ensure that the converted code binds to them
    prolog_load_context(variable_names, SourceVariables),
    % Transform any terms of the form sayText(_) to sayList([...])
    convertDoList(SourceVariables, DoList, [], NewDoList),
    Replacement = htnMethod(if(IfList), do(NewDoList)).
% SayLanguage.pl
:- use_module(library(dcg/basics)).

% DCG Parser for simple "say" language
%
% The language converts a string like "This is a [Value], you see!"
% into:
% - sayText("String") for all the parts outside of any [] and
% - sayCommand(Term) for any term within a [].
%    All variables within Term are unified with variables 
%    in SourceVariables('Name' = Variable, ...)
%    so that they can be used in term expansion and 
%    match the variables in the source by name
%
% e.g. sayLanguageToList(
%              ['Value'=Value],  
%              "This is a [Value], you see!", 
%              [sayText("This is a "), 
%               sayCommand(Value), 
%               sayText(", you see!")]).
sayLanguageToList(SourceVariables, Text, List) :-
    atom_codes(Text, Codes), 
    phrase(values(List, SourceVariables), Codes).

% "values" handles any sequence of one or more "value" 
% (described below)
values([Value|Values], SourceVariables) --> 
    value(Value, SourceVariables), 
    values(Values, SourceVariables), 
    !.
values([Value], SourceVariables) --> 
    value(Value, SourceVariables).
values([], _) --> 
    [].

% a "value" is is a text string or [prologTerm]
% First handle a text string:
value(sayString(String), _) --> 
    string_without([91], Codes), 
    {  
        Codes \== [], 
        string_codes(String, Codes) 
    }.

% Then handle any Prolog term surrounded by []
value(sayCommand(Term), SourceVariables) -->
    [91], string_without([93], Codes), [93],
    {
        read_term_from_codes(Codes, Term, [variable_names(Vars)]),
        % Make sure any variables that exist in source outside 
        % this code are used if they have the same name
        assignTo(Vars, SourceVariables)
    }.


% Unify variables in the list ['Name'=OriginalVar, ...] to
% a new variable from the source in the list ['Name'=SourceVar, ...]
% I couldn't figure out a way to do side-effecting 
% variable assignments using bagof, findall, foreach, etc 
% but recursion worked so:
assignTo([], _).
assignTo([Name=OrigVar | Rest], ListOfAssignments) :-
    member(Name=AssignedVar, ListOfAssignments),
    OrigVar = AssignedVar,
    assignTo(Rest, ListOfAssignments).

Finally, the reason why you get a warning like this:

?- consult("Test.pl").
Warning: Test.pl:3:
Warning:    Singleton variables: [X]
true.

is that the parser doesnā€™t check to see if the goal expansion includes the variables from the source so it thinks it is a singleton. The user can work around it by naming the variable starting with _ followed by a number and then any text like this:

% Test.pl, now with no warnings!
test :- htnMethod(
        if([findItem(_1X)]),
        do([sayText("I found item [_1X]")])
    ).

...

?- consult("Test.pl.pl").
true.

?- listing(test).
test :-
    htnMethod(if([findItem(A)]),
              do([sayList([sayString("I found item "), 
                                 sayCommand(A)])])).

true.

Thanks for all the help!