21

ISO-Prolog (ISO/IEC 13211-1:1995 including Cor.1:2007, Cor.2:2012) offers the following built-in predicates for testing the type of a term:

8.3 Type testing

1 var/1. 2 atom/1. 3 integer/1. 4 float/1. 5 atomic/1. 6 compound/1. 7 nonvar/1. 8 number/1. 9 callable/1. 10 ground/1. 11 acyclic_term/1.

Within this group there are those whose purpose is solely to test for a certain instantiation, that is 8.3.1 var/1, 8.3.7 nonvar/1, 8.3.10 ground/1, and those that assume that a term is sufficiently instantiated such that the type test is safe. Unfortunately, they are combined with testing for a concrete instantiation.

Consider the goal integer(X) which fails if X is a nonvar term that is not an integer and when X is a variable. This destroys many desirable declarative properties:

?- X = 1, integer(X).
   true.
?- integer(X), X = 1.
   false.

Ideally the second query would either succeed using some form of coroutining ; or it would issue an instantiation error1 according to the error classification. After all:

7.12.2 Error classification

Errors are classified according to the form of Error_term:

a) There shall be an Instantiation Error when an
argument or one of its components is a variable, and an
instantiated argument or component is required. It has
the form instantiation_error.

...

Note that this implicit combination of instantiation testing and type testing leads to many errors in Prolog programs and also here on SO.

A quick fix to this situation would be to add an explicit test in front of every test built-in, either verbosely as

   ( nonvar(T) -> true ; throw(error(instantiation_error,_)) ),
   integer(T), ....

or more compactly as

functor(T, _,_),
integer(T), ....

it could be even

T =.. _,
integer(T), ...

My question is twofold:

How to provide this functionality on the user level?

and, to make this also a bit challenging:

What is the most compact implementation of a safer atomic/1 written in ISO-Prolog?


1 Other less desirable options would be to loop or to produce a resource error. Still preferable to an incorrect result.
false
  • 10,264
  • 13
  • 101
  • 209
  • When you say, "how to provide this functionality of the user level", do you mean, "what should the semantics of a predicate be that alleviates the problem?", or "how should `integer/1` be implemented instead?" –  Jan 08 '15 at 12:06
  • @Boris: How should a user see this functionality. What predicates etc. – false Jan 08 '15 at 12:30
  • 1
    How does `must_be(integer, X)` fit into this (to just give an example)? –  Jan 08 '15 at 12:46
  • @Boris: `must_be(integer, a)` produces a `type_error(integer,a)` - it should fail. – false Jan 08 '15 at 12:50
  • So were you thinking of something along the lines of `is_integer(X) :- ground(X), integer(X).` or `is_integer(X) :- atomic(X), integer(X).`, or rather a predicate that will prevent a variable being, or becoming, anything but an integer? –  Jan 08 '15 at 12:53
  • See my question: Ideally ... – false Jan 08 '15 at 12:56
  • @false. I do not understand your above comment (Jan 8 at 12:50). Why should `must_be(integer,a)` fail? According to the manual page, `must_be` never fails. There is some other predicate that can fail. (has_type/2 IIRC). – repeat Jun 02 '15 at 17:00
  • @repeat: A type test that produces an error cannot be used for programming a pure predicate, for you would need to `catch/3` the error. Type errors are nice if you say TINA, but if there is an alternative, they are useless. – false Jun 02 '15 at 17:37
  • Is this due to a merging of two aspects: predicates which test the (never-changing, just as-yet unexplored and unscanned) Problem Space (as in `integer(X)`) and predicates which test the (ever-changing, and always completely known) Computational State (as in `var(X)`). – David Tonhofer Mar 01 '20 at 14:59
  • @false Sorry, I meant that `integer(X)` fails if `X` is not sufficiently instantiated. If `integer(X)` is about the computational state, failing on unbound `X` is sounds ok ("at this state in processing, `X` is required to be bound to an integer"), but if it is about the search in the problem space it should suspend ("`X` is constrained to be an integer for all future considered solutions; if `X` is unbound now, check again when it is bound.") Makes sense? – David Tonhofer Mar 01 '20 at 21:12
  • 1
    @David: An answer `false` to `integer(X)` means that there is no integer. The reason `integer(X)` fails is that DEC10 did not have errors, whereas its predecessor (Marseille) Prolog I did have errors, but nobody read the (French) papers. – false Mar 01 '20 at 21:23

2 Answers2

7

The testing for types needs to distinguish itself from the traditional "type testing" built-ins that implicitly also test for a sufficient instantiation. So we effectively test only for sufficiently instantiated terms (si). And if they are not sufficiently instantiated, an appropriate error is issued.

For a type nn, there is thus a type testing predicate nn_si/1 with the only error condition

a) If there is a θ and σ such that nn_si(Xθ) is true and nn_si(Xσ) is false
instantiation_error.

atom_si(A) :-
   functor(A, _, 0),    % for the instantiation error
   atom(A).

integer_si(I) :-
   functor(I, _, 0),
   integer(I).

atomic_si(AC) :-
   functor(AC,_,0).

list_si(L) :-
   \+ \+ length(L, _),  % for silent failure
   sort(L, _).          % for the instantiation error

character_si(Ch) :-
   functor(Ch,Ch,0),
   atom(Ch),
   atom_length(Ch,1).

chars_si(Chs) :-
   \+ \+ length(Chs,_),
   \+ ( once(length(Chs,_)), member(Ch,Chs), nonvar(Ch), \+ character_si(Ch) ),
   \+ ( member(Ch,Chs), \+ character_si(Ch) ). % for the instantiation error

This is available as library(si) in Scryer.

In SWI, due to its differing behavior in length/2, use rather:

list_si(L) :-
    '$skip_list'(_, L, T),
    functor(T,_,_),
    T == [].
false
  • 10,264
  • 13
  • 101
  • 209
  • @j4nbur53: The motivation was: `throw/1` would require that some value is given for the second argument of `error/2`. However, that value is **implementation defined** which means that one has to indicate a specific value for each implementation. Not even `_` would do. – false Sep 05 '15 at 17:11
  • @j4nbur53: Excellent point. Above code assumes a conforming system. – false Sep 06 '15 at 10:04
  • Your observation is inaccurate for SICStus: Until recently, it produced an instantiation error for `throw(error(type_error(list,1),_)).` Now, it shows the error term literally whereas, say `_=..1.` produces a nicely formatted error. – false Sep 06 '15 at 10:11
  • I cannot see what should be misleading with `error(instantiation_error,functor/3)`. Only the first argument is of relevance here, anyway.... – false Sep 06 '15 at 10:13
  • @j4nbur53: That's irrelevant: Only the first argument of `error/2` is of interest. – false Sep 06 '15 at 16:46
2

This is a very naive attempt at implementing both your suggested solutions.

First, has_type(Type, Var) that succeeds, or fails with an instantiation error:

has_type(Type, X) :-
    var(X), !,
    throw(error(instantiation_error, _)).
has_type(Type, X) :-
    nonvar_has_type(Type, X).

nonvar_has_type(atom, X) :- atom(X).
nonvar_has_type(integer, X) :- integer(X).
nonvar_has_type(compound, X) :- compound(X).
% etc

Second, a could_be(Type, Var) (analogy to must_be/2) that uses coroutining to allow the query to succeed at some point in the future:

could_be(Type, X) :-
    var(X), !,
    freeze_type(Type, X).
could_be(Type, X) :-
    nonvar_has_type(Type, X).

freeze_type(integer, X) :- freeze(X, integer(X)).
freeze_type(atom, X) :- freeze(X, atom(X)).
freeze_type(compound, X) :- freeze(X, compound(X)).
% etc

There are several weak points to this approach but your comments might help me understand the use cases better.

EDIT: On "types" in Prolog

Types in Prolog, as I understand them, are not "types": they are just information that can be queried at run time, and which exists because it is a useful leaky abstraction of the underlying implementation.

The only way I have been able to make practical use of a "type" has been to "tag" my variables, as in the compound terms number(1), number(pi), operator(+), date(2015, 1, 8), and so on. I can then put variables in there, write deterministic or semi-deterministic predicates, understand what my code means when I see it a week later....

So a free variable and an integer are just terms; mostly because, as your question very smartly points out, a free variable can become an integer, or an atom, or a compound term. You could use coroutining to make sure that a free variable can only become a certain "type" of term later, but this is still inferior to using compound terms, from a practical point of view.

It highly likely that I am confounding very different issues here; and to be honest, my experience with Prolog is limited at best. I just read the documentation of the implementation I am using, and try to find out the best way to use it to my advantage.

  • `must_be/2` has a lot of "types" that are not types at all. – false Jan 08 '15 at 13:46
  • @false Yes; I also purposefully left the type "unspecified", so that ad hoc types can be added by the user, if necessary. The type tests in Prolog don't exactly give Prolog a type system (right? not sure about this). Does replacing `must_be` with a `throw(error, ...)` move it closer to what you are looking for? –  Jan 08 '15 at 13:51
  • Or rather: take the viewpoint of the user. Do you want to write `has_type(integer,X)` in place of `integer(X)`? – false Jan 08 '15 at 13:54
  • @false Yes. Since a free variable and an integer have a defined order in the standard order of terms, this is unavoidable. Whether it makes any sense to compare a free variable with an integer is a whole other question. (No, I don't think it makes sense) –  Jan 08 '15 at 13:57
  • You must be misunderstanding here something. What does this question has to do with the standard order of terms? – false Jan 08 '15 at 14:01
  • @false Yes, I must be misunderstanding something. If two objects have a defined (strict) ordering, this puts them in the same domain. If you can look at two objects, `a` and `b`, in that order, and determine which one is the "minimum" of the two (and `a` should be the minimum if they are identical in respect to the ordering), then obviously, they share a "type". From here on, the only sensible approach (I foolishly think) is to either: 1) accept that `integer(X)` is a test for a property, not a type, or 2) that a free variable and an integer should not be ordered. –  Jan 08 '15 at 14:09
  • I still do not see how you brought up this ordering which is an entirely different subject altogether. The fact that `X @< a` is entirely unrelated to the types we are considering here. I admit that this is confusing, see also [this](http://stackoverflow.com/questions/24017420/which-meanings-of-type-are-used-in-the-standard). Which PL has this burden on having two different notions of types intertwined... – false Jan 08 '15 at 14:18