pack_errors.pl -- Contextual error handling for packs

This is a stoics.infrastructure pack that

  1. implements the mid layer for handling Prolog errors
  2. provides a simple, uniform way for displaying originating pack/module and predicate
  3. includes useful pre-canned errors
  4. incorporates error related predicates
  5. decouples the type of printing from the execution behaviour and controls both aspects through simple options

Version 0.3 introduced type errors via type/3 on top of must_be/2.
Version 2.0 has been re-written to be Options centric, fully decoupled and introduced of_same_length/3.

The pack manage mid-level error handling in a uniform way so other packs can use SWI's infracture in a simple way. The user only needs to define the print messages (if the pre-canned ones are not suitable) and then throw the appropriate terms during execution.

Two simple ways to identifying originating caller are provided by allowing options in either the message, or via using an new version of throw, throw/2.

In addition the library includes a number or pre-canned messages and has evolved to provide some error related predicates.

Throwing pack errors

Any term recognised as the first argument of the defined message/3 can be made to spit
a token identifying the originating pack/module and predicate. The main intuition is that this is the
the predicate responsible for the error. You can do this by either wrapping the message or by using
pack_error's own version of throw, throw/2.

Wrapping is via pack_error/2 where the first argument is the message and second is a list of options.

?- throw( pack_error(lengths_mismatch(a,b,1,2),[]) ).
ERROR: Lists for a and b have mismatching lengths: 1 and 2 respectively

?- throw( pack_error(lengths_mismatch(a,b,1,2),[foo:bar/1]) ).
ERROR: foo:bar/1: Lists for a and b have mismatching lengths: 1 and 2 respectively

You can also use throw/2, which is defined in the pack, without wrapping the Message,

?- throw( lengths_mismatch(a,b,1,2), [foo:bar/1] ).
ERROR: foo:bar/1: Lists for a and b have mismatching lengths: 1 and 2 respectively

In both cases, you can drop the list if it contains a single element, thus

?- throw( lengths_mismatch(a,b,1,2), foo:bar/1 ).
ERROR: foo:bar/1: Lists for a and b have mismatching lengths: 1 and 2 respectively

Note that in the latter case (throw/2) the options can also contain terms controling the execution of throw/2.

Options in both cases provide the context:

Pname / Arity
predicate for decoration
Mod:Pname/Arity
prefixed predicate and decoration
pack(Pack)
pack of the originating predicate
pred(Pname/Arity)
alternative have for identifying the predicate

The library is loosely designed around the principle that most packs will define a homonym module. If both Pack and Mod are given and are the same only one is printed, however if they differ, they will both be shown. The order of identification is that of going throough the list above from top to bottom. The first one matching will identify the predicate and stop looking, so alternatives will be ignored.

Prepacked errors

Argument errors.
Poss is a list of (argument) positions and Args a list of arguments.

Printing of Arg(s) itself can be surpressed with prolog_flag(pack_errors_arg,false)- useful for long data.

Other errors

Examples

?- throw( pack_error(arg_ground(3,name), os:os_ext/3) ).
ERROR: os:os_ext/3: Ground argument expected at position: 3,  (found: name)

?- throw( pack_error(arg_ground(3,name(_)), os:os_ext/3) ).
ERROR: os:os_ext/3: Ground argument expected at position: 3,  (found: name(_4210))

?- set_prolog_flag(pack_errors_arg,true).   % this is the default, so no change in behaviour:

?- throw( pack_error(arg_ground(3,name(_)), os:os_ext/3) ).
ERROR: os:os_ext/3: Ground argument expected at position: 3,  (found: name(_4210))

?- set_prolog_flag(pack_errors_arg,false).

?- throw( pack_error(arg_ground(3,name(_)), os:os_ext/3) ).
ERROR: os:os_ext/3: Ground argument expected at position: 3

?- set_prolog_flag(pack_errors_arg,true).

?- throw( pack_error(arg_enumerate(3,[a,b,c],d), [pack(os),pred(os_pred/3)]) ).
ERROR: os:os_pred/3: Term at position: 3, is not one of: [a,b,c], (found: d)

% use throw/2 as it makes code clearer:
?- throw( arg_enumerate(3,[a,b,c],d), os:os_pred/3 ).
ERROR: os:os_pred/3: Term at position: 3, is not one of: [a,b,c], (found: d)

?- throw( arg_enumerate(3,[a,b,c],d), [pack(os),os_pred/3] ).
ERROR: os:os_pred/3: Term at position: 3, is not one of: [a,b,c], (found: d)

?- throw( lengths_mismatch(a,b,1,2), [pack(foo)] ).
ERROR: foo:_Unk: Lists for a and b have mismatching lengths: 1 and 2 respectively

?- throw( lengths_mismatch(a,b,1,2), pred(bar/1) ).
ERROR: _Unk:bar/1: Lists for a and b have mismatching lengths: 1 and 2 respectively

?- throw( cast(abc('file.csv'),atom), os:os_term/2 ).
ERROR: os:os_term/2: Cannot cast: abc(file.csv), to type: atom

Examples from other packs:

?- map_list_options( plus_one, In, [2,3,4,5], [add_options(maybe),on_fail(skip)] ).
ERROR: false:map_list_options/4 @ option(add_options): Object of type: boolean, expected but found term: maybe

Defining new pack errors

Example file (available at pack('pack_errors/examples/fold_data_errors.pl')):

:- multifile( pack_errors:message/3 ).

pack_errors:message( fold_data_insufficient(Dlen,N) ) -->
    ['Insufficient length of data (~d) as ~d folds are required'-[Dlen,N]].
pack_errors:message( fold_data_residual(Dlen) ) -->
    ['Residual data of length: ~d while splitting folds'-[Dlen]].

Load and try with

?- [pack('pack_errors/examples/fold_data_errors')].

?- throw( pack_error(fold_data_insufficient(10,20),true) ).
ERROR: Insufficient length of data (10) as 20 folds are required

?- throw( fold_data_insufficient(10,20), mlu:ten_fold/3 ).
ERROR: mlu:ten_fold/3: Insufficient length of data (10) as 20 folds are required

Pack info

The library reacts to debug(pack_errors) spitting informational message along the execution of library predicates.

Pack predicates:

Pack defined errors selection: (see pack('pack_errors/prolog/pack_errors.pl') for a full list)

author
- nicos angelopoulos
version
- 0.1 2016/01/30
- 0.2 2016/02/24
- 0.3 2017/03/06
- 2.0 2018/10/01
- 2.1 2019/4/22
- 2.2 2022/12/29
See also
- http://stoics.org.uk/~nicos/sware/pack_errors
license
- MIT
 caught(+Goal, +Error, +Opts)
Catches all errors and failure of Goal. The idea is that all non-successful executions are handled identical by the call. If Goal errors, the primary thrown ball is caught and discarded. If Goal errors or fails, behaviour depends on option value OnExit (see Opts below).

Opts

ball(Ball)
instantiates the original exception Ball caught from calling Goal. (So that parts of it can be included in Error.)
on_exit(OnExit=error)
what to do on failed and errored executions
true
succeeds and repors nothing
fail
reports nothing but call itself fails
error
throws the error (any unrecognised value defaults to error)
on_true(OnTrue=true)
call OnTrue iff Goal was successful (and no handling was done)
?- caught( fail, my_exception(on_data), true ).
ERROR: Unhandled exception: pack_error(my_exception(on_data),[on_exit(error),message(error)])

?- caught( fail, my_exception(on_data), on_exit(true) ).
false
% it fails because the message writing fails, which is probably best

?- caught( false,  os_exists_not(abc), [] ).
ERROR: OS entity: abc, does not exist

?- caught( false,  os_exists_not(abc), on_exit(error) ).
ERROR: OS entity: abc, does not exist

?- caught( false,  os_exists_not(abc), on_exit(fail) ).
ERROR: OS entity: abc, does not exist
false.

?- caught( false,  os_exists_not(abc), on_exit(true) ).
ERROR: OS entity: abc, does not exist
true.
See also
- throw/2
 ground(+Term, -Groundness)
 ground_binary(+Term, -Groundness)
Instantiates groundness of Term to Type. In ground_binary/2 Groundness partial and false are collapsed to false.

Groundness

true
Term is ground
false
Term is variable
partial
Term is partially instantiated
?- ground( abc, Abc ), ground( de(F), Def ), ground( GHI, Ghi ).
Abc = true,
Def = partial,
Ghi = false.

?- ground_binary( abc, Abc ), ground_binary( de(F), Def ), ground_binary( GHI, Ghi ).
Abc = true,
Def = Ghi, Ghi = false.
 throw(+Error, +Opts)
An optionised version of throw/1. The Error is not always thrown (eg OnThrow==false, see Opts below).
This version of throw() decouples type of message printing and execution behaviour.<br>

As of version 0.3 this should be the adviced entry point for message writing and ball throwing for stoics packs.

Opts

err(Err=error)
convenenience option that sets both Level and OnExit, if they are absent
error
Level = error and OnExit = error
test
Level=quiet and OnExit = false
exists
Level=warning and OnExit = false
on_exit(OnExit=error)
defines execution behaviour on exiting the printing of the error. One of [true,false,error]. if not given the default depends on Err,
true
succeed
false
fails
error
errors
Pid
predicate indicator (foo:bar/1 or bar/2)
pack(Pack)
originator pack/module
pred(Pid)
originator predicate
option(Opt)
name of originator option
message(Level=error)
passed to print_message/2 (first argument), but also accepts quiet (as silent still prints things...)
?-
    throw( cast(abc('file.csv'),atom) ).

ERROR: Unhandled exception: cast(abc(file.csv),atom)

?-
    throw( pack_error(cast(abc('file.csv'),atom),true) ).

ERROR: Cannot cast: abc(file.csv), to type: atom

?-
    Opt = os:os_exists/2,
    throw(pack_error(cast(abc('file.csv'),atom),Opt)), writeln(later).

ERROR: os:os_exists/2: Cannot cast: abc(file.csv), to type: atom

?-
    throw(cast(abc('file.csv'),atom), os:os_exists/2), writeln(later).

ERROR: os:os_exists/2: Cannot cast: abc(file.csv), to type: atom

?-
    throw(cast(abc('file.csv'),atom), err(test)), writeln(later).

false.

?-
    _Opts = [message(quiet),on_exit(true)],
    throw(cast(abc('file.csv'),atom), _Opts), writeln(later).

later
true.

?-
    _Opts = [message(warning),on_exit(true)],
    throw(cast(abc('file.csv'),atom), _Opts), writeln(later).

Warning: Cannot cast: abc(file.csv), to type: atom
later
true.

?-
    _Opts = [message(informational),on_exit(true)],
    throw(cast(abc('file.csv'),atom), _Opts), writeln(later).

% Cannot cast: abc(file.csv), to type: atom
later
true.

?-
   _Opts = [message(warning),on_exit(false)],
   throw(cast(abc('file.csv'),atom), _Opts), writeln(later).

Warning: Cannot cast: abc(file.csv), to type: atom
false.

?-
    throw(cast(abc('file.csv'),atom), err(exists)), writeln(later).

Warning: Cannot cast: abc(file.csv), to type: atom
false.

?-
    throw(cast(abc('file.csv'),atom), on_exit(true)), writeln(later).

ERROR: Cannot cast: abc(file.csv), to type: atom
later
true.

?-
    throw(cast(abc('file.csv'),atom), on_exit(false)), writeln(later).

ERROR: Cannot cast: abc(file.csv), to type: atom
false.

?-
    throw(cast(abc('file.csv'),atom), on_exit(error)), writeln(later).

ERROR: Cannot cast: abc(file.csv), to type: atom

?-
    throw(cast(abc('file.csv'),atom), message(warning)), writeln(later).

Warning: Cannot cast: abc(file.csv), to type: atom

?-
    throw(cast(abc('file.csv'),atom), message(informational)), writeln(later).

% Cannot cast: abc(file.csv), to type: atom
later
true.

?-
    _Opts = [message(informational),on_exit(false)],
    throw(cast(abc('file.csv'),atom), _Opts), writeln(later).

% Cannot cast: abc(file.csv), to type: atom
false.

?-
    _Opts = [message(informational),on_exit(error)],
    throw(cast(abc('file.csv'),atom), _Opts), writeln(later).

% Cannot cast: abc(file.csv), to type: atom
author
- nicos angelopoulos
version
- 0.2 2017/3/6
- 0.3 2018/1/5 added tracer options: pack, pred & pack_format
- 0.4 2018/9/30 severe re-write, see docs
 type(+Type, @Term)
 type(+Type, @Term, +Opts)
type/2 is a superset of must_be, in that it adds Type = @(Callable), (equiv: Type = call(Callable)), which will succeed iff call( Callable, Term ) succeeds. It also enhances must_be/2 by adding options. In the case of a call-wrapped type, the call to type/3 will succeed iff call(Callable,Term) succeeds.

Opts (unlisted is ok)

error(Err=true)
when false, call fails instead of throwing error
arg(Err=true)
some argument position of Term. (false is reserved and prints no info.)
pack(Pack=false)
when given, the error contains info on the pack throwing the error (false is reserved and prints no info)
pred(Pred=false)
when given, the error contains info on the predicate throwing the error (false is reserved and prints no info)
?- type( boolean, maybe ).
ERROR: Object of type: boolean, expected but found term: maybe

?- type( boolean, maybe, error(false) ).
false.

?- type( boolean, maybe, pack(sure) ).
ERROR: pack(sure): Object of type: boolean, expected but found term: maybe

?- type( boolean, maybe, [pack(sure),pred(lost/2)] ).
ERROR: sure:lost/2: Object of type: boolean, expected but found term: maybe

?- type( boolean, maybe, [pack(sure),pred(lost/2+3)] ).
ERROR: sure:lost/2+3: Object of type: boolean, expected but found term: maybe

?- type( boolean, maybe, [pack(sure),pred(1+lost/2)] ).
ERROR: sure:1+lost/2: Object of type: boolean, expected but found term: maybe

?- type( boolean, maybe, [pack(sure),pred(lost(arg1)/2)] ).
ERROR: sure:lost(arg1)/2: Object of type: boolean, expected but found term: maybe
 defined(+Pid, +From, +Opts)
Throws an error if Pid is not defined in current context.
From is the source from where Pid was supposed to be loaded.
This predicate can act independently (particularly with load(true))
or be combined with pack(lib)'s lib(suggests(Pack)) to, on-demand,
pinpoint to which library is missing and what
predicate within that pack is the deal breaker.

Note that pack(lib) also provides

lib(suggests(Pid,Load))

which is an alternative and more automatic way to achieve demand driven loading via hot-swapping.

:- lib(suggests(Pack))

silently fails if Pack is not present. This is intendent for dependendencies that do not impact major parts for the importing pack. Thus allow common use without grabbing all dependencies that may not be needed for a particular user.

Opts are passed to throw/2, except for: load(Load=false)

?- defined( abc/0, pack(b_real) ).
ERROR: Predicate: abc/0 is not defined (source apparently available at: pack(b_real); not asked to load)

?- defined( abc/0, false ).
ERROR: Predicate: abc/0 is not defined

?- defined( abc/0, false, pack(sourcey) ).
ERROR: sourcey:$unknown/0: Predicate: abc/0 is not defined

?- defined( abc/0, pack(b_real), [pack(sourcey),pred(foo/1;2)] ).
ERROR: sourcey:foo/1;2: Predicate: abc/0 is not defined (source apparently available at: pack(b_real); not asked to load)

?- defined( b_real/0, pack(b_real), [as_pack_err(true),load(library(b_real))] ).
true.

The above only succeeds if b_real is an install library and defines b_real/0.

From or Load can have the special form: lib(CodeLib). This assumes pack(lib) is installed and lib/1 will be used to load the requested CodeLib.

?- defined( b_real/0, lib(b_real), load(true) ).

Will again, only succeed if b_real is installed and defines b_real/0. In this occasion library(lib) should be also installed.

author
- nicos angelopoulos
version
- 0.1 2018/1/5
See also
- throw/2
- lib/1 (lib(suggests/1)) can work with this predicate
- lib/1 (lib(suggests/2)) as an alternative
 pack_errors
This is a documentation predicate, providing an anchor for documentation pointers.
 of_same_length(+Lists)
 of_same_length(+List1, +List2)
of_same_length(+Lists, +Opts)
 of_same_length(+List1, +List2, +Opts)
Generic sanity predicate, checking that two or more lists are of the same length.

In order to disambiguate between the two versions of the arity 2, in that scenario options should be a term of the form opts(OptsL).

Opts are passed to throw/2, the only local one is:

?- of_same_length( [a,b,c], [1,2,3] ).
true.

?- of_same_length( [[a,b,c],[1,2,3]] ).
true.

?- of_same_length( [1,2,3], [a,b], token1(first) ).
ERROR: Lists for first and 2 have mismatching lengths: 3 and 2 respectively
author
- nicos angelopoulos
version
- 0.1 2018/09/24
 pack_errors_version(-Version, -Date)
Current version and release date for the library.
V = 2:2:0,
D = date(2022, 12, 29).
author
- nicos angelopoulos
version
- 2:2 2022/12/29

Undocumented predicates

The following predicates are exported, but not or incorrectly documented.

 defined(Arg1, Arg2)