How to create bidirectional predicates in Prolog?

In Prolog, there are predicates that are bidirectional (or even "multidirectional"), in the sense that you can divide a set of variables into input and output variables in many different ways. For example, the predicate number_string acts as a bijection in both directions:

 number_string(N, "42"). % finds solution N = 42 number_string(42, S). % finds solution S = "42" 

However, this remarkable property does not seem to be preserved as a result of the combination of items. For example, consider the following two predicates that simply translate strings, such as 3x^2 into fragments of an abstract syntax tree, such as term(3,2) :

 parse_stuff(String, term(Coeff, Pow)) :- string_concat(CoeffStr, MonomialStr, String), string_concat("x^", PowStr, MonomialStr), number_string(Coeff, CoeffStr), number_string(Pow, PowStr). write_stuff(String, term(Coeff, Pow)) :- number_string(Pow, PowStr), number_string(Coeff, CoeffStr), string_concat("x^", PowStr, MonomialStr), string_concat(CoeffStr, MonomialStr, String). 

Both predicates are exactly the same, except that the order of the defining sentences on the right is reversed. All items on the right are bi-directional.

Here is an example session:

 ?- parse_stuff("3x^2", X). X = term(3, 2). ?- parse_stuff(X, term(3, 2)). ERROR: string_concat/3: Arguments are not sufficiently instantiated ?- write_stuff(X, term(3, 2)). X = "3x^2". ?- write_stuff("3x^2", X). ERROR: number_string/2: Arguments are not sufficiently instantiated 

Both predicates work in only one direction, although both of them are obviously bijections, which is rather sad.

The question consists of two parts (there are two requirements: ease of use, ease of maintenance):

  1. Is there a reliable way to build bi-directional predicates in Prolog?
  2. Is it possible to avoid duplication of code (i.e., support two versions: one normal, one upside down)?

Maybe there is some kind of flag that says "if necessary, change the order of sentences"?

I am currently using SWI-Prolog 7.1.x, if relevant.


↓ Go to my own answer ↓

+8
source share
3 answers

There are two ways to solve this: either you use an additional argument indicating that there must be an order, either implement an upper predicate that selects which of your predicates to use based on it, or you write an upper predicate that makes a choice, checking which arguments free, without the need for external information. In the latter case, you are probably using the predefined predicates var/1 or nonvar/1 ; I used this extensively, making the Definite-Clause relational algorithms so that they could be used for parsing and generating text, thereby avoiding a lot of work both when writing programs and when saving them.

UPDATE in response to an additional requirement.

To avoid code duplication, you should try to define common parts in both versions and define predicates for each part. Then you call these predicates where necessary, instead of having your code in many places. But this is not very useful in your example, which is too small: perhaps you can try using difference lists and avoid concatenations to simplify your predicates.

+3
source

Systematic bijection binding [My own answer]

Benefits:

  • Produces a bi-directional predicate from a bijection predicate chain.
  • Prevents code duplication.
  • No explicit order specification is required.

Disadvantages:

  • Suppresses warnings about unused free variables
  • Uses reflection (can affect compiled code performance)

Here's how we can relate several predicates, which are obviously bijections:

 % suppresses "Singleton variables" warning for % a single variable suppress_singleton_warning(_). % calls all clauses in a list. call_all([]). call_all([F|G]) :- call(F),call_all(G). % If `X` is a closed term, calls all clauses `FS` % in left-to-right order. bijection(X, FS, Y) :- suppress_singleton_warning(Y), free_variables(X, XVars), XVars == [], call_all(FS). % If `Y` is a closed term, calls all clauses `FS` % in right-to-left order bijection(X, FS, Y) :- suppress_singleton_warning(X), free_variables(Y, YVars), YVars == [], reverse(FS, RevFS), call_all(RevFS). 

Usage example:

 % Example: parser/printer that works in both % directions. parse_stuff(String, term(Coeff, Pow)) :- Clauses = [ string_concat(CoeffStr, MonomialStr, String), string_concat("x^", PowStr, MonomialStr), number_string(Coeff, CoeffStr), number_string(Pow, PowStr) ], bijection(String, Clauses, term(Coeff, Pow)). 

You can also simply pass Clauses directly; no additional variable is required. This works as expected in both directions:

  parse_stuff("3x^2", T), write(T), nl, parse_stuff(X, term(3,2)), write(X), nl 

gives:

 term(3,2) 3x^2 
+2
source

I stumbled upon a similar snag, writing a sentence to give the numbers the prefix 'n' so that they could be used as functors that I later wanted to convert back, say, from n1 to 1. I got this somewhat ineffective solution that works:

 number_to_identifier(Number, Identifier) :- ground(Number) -> (number(Number), number_string(Number, String1), string_concat('n', String1, String2), atom_string(Identifier, String2)) ; (atom(Identifier), atom_string(Identifier, String2), string_concat('n', String1, String2), number_string(Number, String1)). 
0
source

Source: https://habr.com/ru/post/986246/


All Articles