Search code examples
prolog

Make a predicate reversible


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?


Solution

  • 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.