Good to know about freeze, thanks. But still not at useful as can_be for this purpose. For example, this will cause problems (even though L can be a list):
Yes, there ought to be “freeze” versions of the various must_be/2 predicates, and proper_list would need to be implemented differently than freeze(L, must_be(proper_list, L)).
(If can_be/2 does nothing when the variable is uninstantiated, then it’s rather misleading.)
What is the meaning of can_be(list, X), X = an_atom? If can_be/2 succeeds if the argument is uninstantiated, then this seems to be logically nonsense (because X = an_atom, can_be(list, X) will fail.
It is definitely not is_not/2. That predicate is part of must_be/2 and responsible for the error message. So, if X is not a list we call is_not(list, X) which will figure out which error to throw (basically an instantiation error or a type error).
The idea of a can_be/2 makes sense. Shouldn’t be too hard to add to library(error). If someone likes to give it a try, I’m happy to consider the PR.
It might better (?) relate to work that is sleeping for a long time and defines Prolog types as constraints. So, you can do ?- type(X, atom), type(Y, integer), X = Y. and it will fail On the other hand, this library is more intended for abstract interpretation/type checking. I assume can_be/2 is no constraint, so we can happily do this?
In the “Power of Prolog” video I linked, Markus Triska explains it as:
Silent failure for wrong types is default practice in Prolog code.
Type errors help us determine the exact location of mistakes.
However, a sound distinction between type errors and instantiation errors is hard to get right manually.
Therefore, if you want type errors, use:
must_be/2 for arguments that must be sufficiently instantiated