ETOOBUSY 🚀 minimal blogging for the impatient
More constraint programming
TL;DR
Inspired by the train of posts about aquarium (see the last one here: Aquarium - exploiting redundant constraints), I decided to encapsulate a tiny constraint programming support function in cglib (my copy-and-paste library for CodinGame).
And you can find it where you think: ConstraintSolver.pm.
Let’s take a look at the function:
1 sub solve_by_constraints {
2 my %args = (@_ && ref($_[0])) ? %{$_[0]} : @_;
3 my @reqs = qw< constraints is_done search_factory start >;
4 exists($args{$_}) || die "missing parameter '$_'" for @reqs;
5 my ($constraints, $done, $factory, $state, @stack) = @args{@reqs};
6 my $logger = $args{logger} // undef;
7 while ('necessary') {
8 last if eval { # eval - constraints might complain loudly...
9 $logger->(validating => $state) if $logger;
10 my $changed = -1;
11 while ($changed != 0) {
12 $changed = 0;
13 $changed += $_->($state) for @$constraints;
14 $logger->(pruned => $state) if $logger;
15 } ## end while ($changed != 0)
16 $done->($state) || (push(@stack, $factory->($state)) && undef);
17 };
18 $logger->(backtrack => $state, $@) if $logger;
19 while (@stack) {
20 last if $stack[-1]->($state);
21 pop @stack;
22 }
23 return unless @stack;
24 } ## end while ('necessary')
25 return $state;
26 } ## end sub solve_by_constraints
Parameters unfolding (lines 2..6) is the same as described in the past: some
arguments are mandatory and an exception is thrown if they are missing. The
only pseudo-clever (read: less readable) concession to conciseness is the
definition of @stack
in line 5: it gets no value, hence it starts empty
(as it is supposed to be).
The whole function is basically a loop that tries hard to find a solution or
bails out if it’s not possible (line 23). In this case, undef
is returned
to signal this error condition.
Lines 8 to 17 are executed in a protected environment because the constraints might complain loudly about the specific condition. This is part of the API for constraints: if anything is not fine, just throw an exception.
Constraints are handled in lines 10 to 15. The sub-loop goes on until
constraints are no more capable of pruning away unneeded branches, which is
tracked by $changed
. This is another aspect of the constraints API: they
are supposed to return 0
if no pruning happened, or a positive value if
some pruning happened (arguably, the number of modifications performed, but
it’s not a hard and fast rule).
When constraints pruning is over, two mutually exclusive conditions might happen:
-
the search is complete and
$state
contains a solution. In this case, line 16 returns a true value (the or side is ignored) and the outer loop will be exited (rememberlast if ...
in line 8). Well done! From there we jump to line 25 and we’re done; -
the search is still not complete. In this case, line 16 will return
undef
, but only after pushing a new exploration function on the stack.$factory
’s API, in fact, is to return a sub reference that will provide successive search hypotheses to investigate at a certain point of the investigation.
In the latter case, the search is not over and the backtrack in lines 18 to 23 kicks in. I know, I know… the very first time is not really backtracking, but we have to do exactly the same operations, so why not? Moreover, it’s possible to understand whether it’s the first time we call the iterator, or it is a real backtracking, by looking at the third parameter of the invoked logger function: upon backtracking it will contain the exception!
If we find something more to investigate (line 20) then we can continue,
otherwise @stack
will be depleted and we will return… nothing (line
23).
This is the skeleton… if you want to use it, you have to provide the meat! Look at ConstraintSolver.pod to look at the API, and wait some more time… for an example 🙄 Until next time… happy coding!
Update: aligned code to latest version, which includes the exception in invoking the logger upon backtracking.