Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Make a predicate reversible

Tags:

prolog

I'm new to prolog; I'm coming from a structured programming background, as will become obvious :)

I am building up a prolog query that involves reversing a number; eg. reverse_num(123,X) results in X = 321. I came up with the following definition, but it only works when I provide a number as the first parameter.

reverse_num(Num, Revnum) :-
  number_chars(Num, Atoms),
  reverse(Revatoms, Atoms),
  number_chars(Reversed, Revatoms),
  Reversed = Revnum.

the number_chars/2 predicate doesn't like an unsubstantiated variable if I do: reverse_num(X,123) (where I'm expecting X to be 321).

Am I trying too hard to make reverse_num do something it shouldn't (should it be understood to work only with a number as the first parameter and variable as the second)?

Or is there an easy / straight-forward way to handle a variable as the first parameter?

like image 735
Jeff Schaller Avatar asked May 31 '15 01:05

Jeff Schaller


People also ask

How does reverse work in Prolog?

Prolog reverse list is defined as the reversing of the given list, it means this operation is used to find reverse of a given list by using prolog programming language, prolog is a logical and declarative programming language, suppose there is a list L=[a,b,c,d,e,f ], and want to reverse the elements of list, so the ...


4 Answers

Relational naming

Before jumping into coding, let's take a step back. After all, the idea in Prolog is to define relations. Your name reverse_num/2 rather suggests some actions, num_reversed/2 might be a better name.

Determine the relation

Your definition is not that bad, let me rewrite it to1:

num_reversed(Num, Reversed) :-
   number_chars(Num, Chars),
   reverse(Chars, Revchars),
   number_chars(Reversed, Revchars).

?- num_reversed(123,X).
   X = 321.
?- num_reversed(1230,X).
   X = 321.
?- num_reversed(12300,X).
   X = 321.

Do you see the pattern? All numbers N*10^I have the same result!

Now, let's ask some more:

?- num_reversed(Num, 321).
   error(instantiation_error,number_chars/2).

Hm, what did we expect? Actually, we wanted all 123*10^I to be printed. That's infinitely many solutions. So above query, if correctly answered, would require infinitely many solutions to be printed. If we print them directly, that will take all our universe's lifetime, and more!

It is for this reason, that Prolog produces an instantiation error instead. By this, Prolog essentially states:

This goal is too general that I can make a good answer. Maybe there are infinitely many solutions, maybe not. I know not. But at least I indicate this by issuing an error. To remove this error you need to instantiate the arguments a bit more.

So the answer Prolog produced was not that bad at all! In fact, it is much better to produce a clean error than to, say, fail incorrectly. In general, Prolog's errors are often a very useful hint to what semantic problems you might have. See all error classes how.

Coroutining

As have other answers suggested, coroutining, using when/2 might solve this problem. However, coroutining itself has many semantic problems. Not without reason, systems like XSB do not offer it, due to the many problems related to subsumption checking. An implementation that would be compatible to it would be unexpectedly inefficient.

But for the sake of the point, we could make our definition more versatile by querying it like

 ?- when(nonvar(Num), num_reversed(Num, Reversed)).
    when(nonvar(Num), num_reversed(Num, Reversed)).

Now we get back as an answer exactly the query we entered. This is also known as floundering. So there is a way to represent infinitely may solutions in a compact manner! However, this comes at a rather high price: You no longer know whether a solution exists or not. Think of:

?- when(nonvar(Num), num_reversed(Num, -1)).
   when(nonvar(Num), num_reversed(Num, -1)).

Others have suggested to wait also for nonvar(Reversed) which would only be correct if we would produce infinitely many answers - but, as we have seen - this just takes too much time.

Coroutining looked as a very promising road at the beginning of the 1980s. However, it has never really caught on as a general programming methodology. Most of the time you get much too much floundering which is just a pain and even more difficult to handle than, say instantiation errors.

However, a more promising offspring of this development are constraints. There, the mechanisms are much cleaner defined. For practical purposes, programmers will only use existing libraries, like CLPFD, CLPQ, or CHR. Implementing your own library is an extremely non-trivial project in its own right. In fact it might even be possible to provide an implementation of num_reversed/2 using library(clpfd) that is, restricting the relation to the integer case.

Mode dependent conditionals

Traditionally, many such problems are solved by testing for instantiations explicitly. It is good style to perform this exclusively with nonvar/1 and ground/1 like the condition in when/2- other type test predicates lead easily to errors as exemplified by another answer.

num_reversed(Num, Reversed) :-
   (  nonvar(Num)
   -> original_num_reversed(Num, Reversed)
   ;  original_num_reversed(Reversed, Base),
      (  Base =:= 0
      -> Num is 0
      ;  length(_, I),
         Num is Base*10^I
      )
   ).

Above code breaks very soon for floats using base 2 and somewhat later for base 10. In fact, with classical base 2 floats, the relation itself does not make much sense.

As for the definition of number_chars/2, ISO/IEC 13211-1:1995 has the following template and mode subclause:

8.16.7.2 Template and modes

number_chars(+number, ?character_list)
number_chars(-number, +character_list)

The first case is when the first argument is instantiated (thus nonvar). The second case, when the first argument is not instantiated. In that case, the second argument has to be instantiated.

Note, however, that due to very similar problems, number_chars/2 is not a relation. As example, Chs = ['0','0'], number_chars(0, Chs) succeeds, whereas number_chars(0, Chs), Chs = ['0','0'] fails.

Very fine print

1 This rewrite is necessary, because in many Prologs reverse/2 only terminates if the first argument is known. And in SWI this rewrite is necessary due to some idiosyncratic inefficiencies.

like image 56
false Avatar answered Oct 20 '22 00:10

false


The number_chars/2 predicate has the signature:

number_chars(?Number, ?CharList) 

But although not fully specified by the signature, at least Number or CharList have to be instantiated. That's where the error occurs from.

If you call:

reverse_num(Num,123)

You will call number_chars/2 with both uninstatiated at that time so the predicate will error.

A not very nice solution to the problem is to ask whether Num or RevNum are number/2s. You can do this by writing two versions. It will furthermore filter other calls like reverse_num(f(a),b), etc.:

reverse_num(Num,Revnum) :-
    \+ number(Num),
    \+ number(Revnum),
    throw(error(instantiation_error, _)).

reverse_num(Num, Revnum) :-
    ground(Num),
    number(Num),
    !,
    number_chars(Num, Atoms),
    reverse(Revatoms, Atoms),
    number_chars(Revnum, Revatoms).

reverse_num(Num, Revnum) :-
    ground(Revnum),
    number(Revnum),
    reverse_num(Revnum,Num).

Or you can in case you use two nongrounds (e.g. reverse_num(X,Y).) an instantiation error instead of false as @false says:

reverse_num(Num,Revnum) :-
    \+ number(Num),
    \+ number(Revnum),
    !,
    throw(error(instantiation_error, _)).

reverse_num(Num, Revnum) :-
    number(Num),
    !,
    number_chars(Num, Atoms),
    reverse(Revatoms, Atoms),
    number_chars(Revnum, Revatoms).

reverse_num(Num, Revnum) :-
    reverse_num(Revnum,Num).

The cut (!) is not behaviorally necessary, but will increase performance a bit. I'm not really a fan of this implementation, but Prolog cannot always fully make predicates reversible since (a) reversibility is an undecidable property because Prolog is Turing complete; and (b) one of the characteristics of Prolog is that the body atoms are evaluated left-to-right. otherwise it will take ages to evaluate some programs. There are logic engines that can do this in an arbitrary order and thus will succeed for this task.

If the predicate/2 is commutative, a solution that can be generalized is the following pattern:

predicate(X,Y) :-
    predicate1(X,A),
    predicate2(A,B),
    % ...
    predicaten(C,Y).
predicate(X,Y) :-
    predicate(Y,X).

But you cannot simply add the last clause to the theory, because it can loop infinitely.

like image 31
Willem Van Onsem Avatar answered Oct 20 '22 02:10

Willem Van Onsem


Nice to see someone is also worried about define flexible rules with no restrictions in the set of bound arguments.

If using a Prolog system that supports coroutining and the when/2 built-in predicate (e.g. SICStus Prolog, SWI-Prolog, or YAP), try as:

reverse_num(Num, Reversed) :-
  when( ( ground(Num); ground(Atoms) ), number_chars(Num, Atoms) ),
  when( ( ground(Reversed); ground(Revatoms) ), number_chars(Reversed, Revatoms) ),
  reverse(Atoms , Revatoms).

that gives:

?- reverse_num( 123, X ).
X = 321.

?- reverse_num( X, 123 ).
X = 321 .

( thanks to persons who provided theses answers: Prolog: missing feature? )

like image 2
pasaba por aqui Avatar answered Oct 20 '22 02:10

pasaba por aqui


This SWISH session shows my effort to answer.

Then I've come back here, where I found I was on @PasabaPorAqui' mood (+1), but I didn't get it right.

But, such an interesting topic: notice how regular is the join pattern.

reverse_num(X, Y) :-
    when((nonvar(Xs);nonvar(Ys)), reverse(Xs, Ys)),
    when((nonvar(X) ;nonvar(Xs)), atomic_chars(X, Xs)),
    when((nonvar(Y) ;nonvar(Ys)), atomic_chars(Y, Ys)).

So, we can generalize in a simple way (after accounting for PasabaPorAqui correction, ground/1 it's the key):

% generalized... thanks Pasaba Por Aqui
:- meta_predicate when_2(0).
when_2(P) :-
    strip_module(P,_,Q),
    Q =.. [_,A0,A1],
    when((ground(A0);ground(A1)), P).

reverse_num(X, Y) :-
    maplist(when_2, [reverse(Xs, Ys), atomic_chars(X, Xs), atomic_chars(Y, Ys)]).

I think I understand why nonvar/1 was problematic: the list bound for reverse get 'fired' too early, when just the head get bound... too fast !

maplist/2 is not really necessary: by hand we can write

reverse_num(X, Y) :-
    when_2(reverse(Xs, Ys)),
    when_2(atomic_chars(X, Xs)),
    when_2(atomic_chars(Y, Ys)).

this seems an ideal application of term rewriting... what do you think about -:- ? Implementing that we could write bidirectional code like

reverse_num(X, Y) -:-
    reverse(Xs, Ys),
    atomic_chars(X, Xs),
    atomic_chars(Y, Ys).

edit SWISH maybe is not 'term_rewrite' friendly... so here is a lower level approach:

:- op(900, xfy, ++).
A ++ B ++ C :- when_2(A), B ++ C.
A ++ B :- when_2(A), when_2(B).

reverse_num(X, Y) :- 
    reverse(Xs, Ys) ++ atomic_chars(X, Xs) ++ atomic_chars(Y, Ys).
like image 1
CapelliC Avatar answered Oct 20 '22 02:10

CapelliC