When to use must_be?
This predicate should be used exclusively to perform contract checks and assertion checks, i.e. to inspect the current computational state of the running program.
Because it only does two things, Succeed or Throw:
- Throw ISO standard
Termis currently a fresh variable (freshvar), aka. "unbound".
- Otherwise throw ISO standard
Termis NOT of
- Otherwise succeed
So it's a tripwire to be used in Design by Contract.
One would use it to check the parameters passed to "API predicates" but generally not among "friendly/helper predicates" (except during development). One may want to rely on other predicates throwing exceptions "late" due to bad arguments instead of check for erroneous arguments "early". But as usual, it depends. Including on your level of paranoia.
Jan Wielemaker writes:
- _must_be/2 is not optimized in any way. It is no more and no less than a cheap and explicit way to add type checks intended primarily for public APIs of libraries. Some people tend to stick it in everywhere. That may seriously slow down the code. I almost never use it in cases where built-in predicates do a reasonable job anyway._
Eric Taucher writes:
- If the must_be/2 is used during development to keep me from making dumb mistakes like passing an empty variable then I leave them in until I know the predicate is working for the production code then strip them out by hand.
- If the must_be/2 is used for code that is checking for resources like a file or database connection that must exist for the code to work then I leave them in, even for the production code.
- _However if during development they are acting as a guard and I want the code to halt as soon as the problem is found but then work as guard with a silent fail once it is in production, I use must_be/2 during development and then change them to is_of_type/2 once the code is production quality._
- There are more scenarios but you should start to get the idea that the goal is to get code that works correctly in production, what happens in development is not set in stone.
- The reverse process also has uses: If production code has a problem or I did not write the code and want to understand it, I bring the code back in for development, change is_of_type/2 to must_be/2, add additional must_be/2 and other such changes.
Disabling must_be calls for production use
Note that one can "disable assertions" in other programming languages, and if they have been used properly, disabling them should not change program semantics.
You can do something similar here, using goal_expansion/2:
Before loading the application:
The predicate seems to be missing a third argument: any context information which might be of interest to the whoever is handling the exception. A must_be/3 might be nice.
The ISO Standard exception, (which is not particularly flexible) allows an
Extra term in the exception term
error(instantiation_error,Extra). Could be used here.
There is even use for an argument that indicates whether we want:
- An exception on test failure
- A failure on test failure
More desirable features:
- One might want to have a disjunction and a conjunction inside the must_be/2 Type field
- The criterium of "nonempty list", including "nonempty list of some type"
- The criterium of "list of 'type or type'"
- The criterium of "a yall lambda expression"
- The criterium of "a compound of name N and arity A"
Various type-testing approaches
Finally, comparing various type-testing approaches with a bit of test code
- Default approach which fails silently if the answer is "don't know"
- "Sufficiently instantiated" approach which throws if the answer is "don't know"
- must_be/2 approach which throws unless the answer is "yes, the type matches" (can be used on predicate entry to check whether a contract regarding input arguments is being upheld)
- can_be/2 approach which throws only if the answer is "it's never going to be that type" (can be used on predicate entry to check whether a contract regarding output arguments is being upheld: is it possible to bind a term of a certain type to that argument later on?)
See also "Type tests in Prolog" by Markus Triska https://youtu.be/ZIv0G4b1xBQ?t=123
Various problems with the whole approach
?- must_be(list(pairs),). true.
The above is an illusion, as the type expression is not currently checked:
?- must_be(list(pairs),[X-N]). ERROR: type `pairs' does not exist
?- must_be(list(pair),[X-N]). true.
?- must_be(list(pair),[N]). ERROR: Arguments are not sufficiently instantiated
How do I express "list of pairs where an unbound variable is also allowed as it could become pair", which is akin "can be list of pairs" but w/o accepting a unbound variable at the list position?
I cannot resort to ";" between must_be/2 calls because must_be throws instead of failing. So one has to do this:
foo(Pairs) :- must_be(list,Pairs), % check it's a list (maplist([X]>>(var(X);X=_-_),Pairs) -> R=true ; R=false), % reify individual checks (\+R -> must_be(list(pair),Pairs) ; true). % pretend it's "not a list of pairs" if the above yields false
Nearly works, but if the bad list entry comes after an unbound variable list entry, this yields the inappropriate message:
?- foo([X,x-y,hh]). ERROR: Arguments are not sufficiently instantiated
Hamcrest matchers, originally from the Java Universe and used primarily in unit tests:
These have the entry point
MatcherAssert.assertThat() and take a Matcher Expression to verify. However, but just return
false instead of throwing - throwing a
java.lang.AssertionError or some other Throwable is left to the caller.
The Hamcrest Matchers for Erlang seem most comparable.
string_is_only_digits_test() -> assert_that("12345", is(only_digits())).