Archive
I left my keys in a side-channel
While trying to bake option groups into a module I stumbled over a neat solution to a common problem. I wanted to have a subroutine as a where-clause that slurps up named arguments, does some checks and either returns True to satisfy the where-clause or to die
with a proper error message. That was simple but incomplete. The where-clause doesn’t provide the name of the argument it is checking on, what would be needed to have a proper error message. Let’s have some (simplified) code.
sub checker(*%colon-keys){ sub ($value-from-where-clause) { True } } sub f( *%h where checker(:a, :b, :c) ) {}
The sub checker
is called before the where clause is actually checking anything. In Perl6 a where-clause is kind of syntaxy. If an expression is provided it will call that expression and keep it’s returned value to then go and do the actual check against the value of %h. This does not happen at compile time and the returned value is not cached. In our example an anonymous sub is returned that takes one argument (a requirement by where) and must return True
to let the where-clause-check pass.
As you can see checker takes a list of colon-pairs. That leaves the question how we can provide additional information we might want to output in an exception, esp. if we want that parameter to be optional and avoid odd syntax. We could have an optional positional but then we can’t mix positional and named arguments in checker
. Storing that value would be trivial because the anonymous sub that is returned could have a closure variable. We just need to fill it. Luckily the sub is returned as a reference that is then called by the where-clause. We can sneak another call in, as long as we we don’t forget to return the code reference to the anonymous sub.
Returning a reference to the same object is done by chainable method calls. Often, when things get complicated in Perl 6-land, we just mix a role in. Then we have a method and can return self
.
use v6; sub g($i){ my $closure-variable; sub ($value-from-where-clause) { say [$closure-variable, $value-from-where-clause]; $value-from-where-clause == $i or die "bad value $value-from-where-clause for $closure-variable" } but role :: { method side-channel($second-value){ $closure-variable = $second-value; self } } } sub f($a where g(42).side-channel($a.VAR.name) ){ 'OK' } say f(42); # OUTPUT # [$a 42] # OK
And there we have it — a side-channel to a where-clause-call or any other spot we can use higher order functions at. Now I can go and provide MTA error messages for option groups.
These keys are LTA
While toying around with enums as boolean options to a routine, i found the default error message less then awesome.
Constraint type check failed for parameter '@options'
It would be hard to be even less specific. Let’s create a few exceptions to tell what is going on when things go wrong.
class X::Paramenter::Exclusive is Exception { has $.type; method message { "Parameters of {$.type.perl} are mutual exclusive" } }
Now we can check if options of Find::Type
are exclusive and complain accordingly.
&& ( exclusive-argument(@options, Find::Type) or fail X::Paramenter::Exclusive.new(type => Find::Type) ) class X::Parameter::UnrecognisedOption is Exception { has $.type; has $.unrecognised; method message { "Option { $.unrecognised } not any of { $.type.map({ (.^name ~ '::') xx * Z~ .enums.keys.flat }).flat.join(', ') }" } }
Since enums are containers for types and those got names we can use set operators to check and single out none matching options (basically anything the +@options
slurps up we don’t know).
or fail X::Parameter::UnrecognisedOption.new(type => (Find::Type, Find::Options), unrecognised => .item ∖ (|Find::Type::.values, |Find::Options::.values) )
Stitching the error message together is a bit more involved because we can get a list of all enum keys in a given enum but those don’t know their qualified name. We have to prefix with the enum name and ::
by hand.
class X::Parameter::UnrecognisedOption is Exception { has $.type; has $.unrecognised; method message { "Option { $.unrecognised } not any of { $.type.map: { (.^name ~ '::') xx * Z~ .enums.keys.flat } }" } }
This results in a much more awesome error message:
Option 42 not any of Type::File, Type::Dir, Type::Symlink, Options::Recursive, Options::Keep-going
This looks all quite regular. We have a slurpy that is kind of parameterised with one or many enums and those enums may have a flag telling if they act like radio buttons. Sounds like this idiom would fit nicely into a module.
Keys are optional
On my quest to a concurrent File::Find I found the need to have arguments (in this case as Bool) that are of a group of sorts and are mutual exclusive. Enums are very groupy, introduce easy to use names into the scope (directly or via is export
) and should be easy to make mutual exclusive. The easy part was a bit naive because typed slurpy arguments are not supported (yet). If there is no easy way, there must be a hard way that is possible.
First let’s define two enums that serve as options to find.
package Find { enum Type (<File Dir Symlink>); enum Options (<Recursive Keep-going>); }
Now we can have a where-clause that first checks if all members of a slurpy array are either of type Find::Type or Find::Options. Then we can check how many elements of Find::Options there are. Since there can be only one we complain about exclusiveness if there are to many.
+@options where { @options.all (elem) (Find::Type::.values (|) Find::Options::.values) && (+(@options.grep: * ~~ Find::Type) <= 1 or die "can only check for one type at a time") }
In the body of the routine we can use junctions and smart matching to check if options are present.
my Bool $recursive = any(@options) ~~ Find::Recursive; my %tests = Find::File => {so .f}, Find::Dir => {so .d}, Find::Symlink => {so .l}; @tests.append(%tests{@options.grep: * ~~ Find::Type});
The routine is then called with a list of flags at the end of it’s parameter list.
find(%*ENV<HOME>, include => {.extension eq 'txt'}, exclude => ['cfg', /.xml $/] , Find::File, Find::Recursive, Find::Keep-going);
The same would be possible to do with named arguments but I can’t see a way to do the exclusiveness in a where-clause. I like to have as much argument processing in the signature because it makes it easy to write documentation. Separate all arguments with a newline and then translate type constraints and where-clauses into plain English. Also, having enums as flags feels quite 6-ish and that’s what this blog (post) is about.
Are these your keys?
One bug that keeps biting me are typos in constant Hash keys. It’s not hard to confine keys to a given list of strings by mixin a role that overloads method AT-KEY
. But that would be a runtime error and replacing one runtime error with another ain’t now good.
Enums do have a constant set of Keys and provide identity a Hash can use. Perl 6 does allow enums as key constraints and will complain at compile time if we ask for a keys that is not defined as a enum key. However, if we confine a Hash to a given set of keys, we may want to output all possible keys, not just the keys that have values associated. We can solve that by mixin in a role.
enum Noms(<Greenstuff Walkingstuff Syntetics>); (my %eaten{Noms} is default(0)) does role :: { method keys { Noms::.values } method kv { gather for self.keys -> \k { take k, self.{k}} } }; %eaten{Greenstuff}++; dd %eaten; # Hash[Any,Noms]+{<anon|75781152>} %eaten = (my Any %{Noms} = Noms::Greenstuff => 1) dd %eaten.keyof; # Noms dd %eaten.keys; # (Noms::Walkingstuff, Noms::Greenstuff, Noms::Syntetics).Seq dd %eaten.kv; # ((Noms::Walkingstuff, 0), (Noms::Greenstuff, 1), (Noms::Syntetics, 0)).Seq
The default value of 0 makes sense for a simple counting Hash. A Failure may be more appropriate for a undefined value, given that we nailed the keys down.
Of cause we can stick those methods into a properly named role and while we are on it, take care about Arrays too. The default value type for enums are Int that start at 0, what is to the liking of Arrays.
role Enumkeys[::T] { method keys { T::.values } multi method kv(Positional:D) { gather for self.keys -> \k { take k, self.[k] } } multi method kv(Associative:D) { gather for self.keys -> \k { take k, self.{k} } } }
If we do use a limited set of keys, we should limit the size of the Array to the number of enum keys.
multi sub prefix:<+>(Any:U \E where .HOW ~~ Metamodel::EnumHOW){ E.enums.elems }
Since Arrays don’t really have have a key value, Perl 6 won’t help use if we use the Array without enum keys but simple typos will be caught at compile time. Also, we need to do a runtime mixin, that’s where the parentheses around the my-statement come from.
(my @eaten[+Noms]) does Enumkeys[Noms]; @eaten[Syntetics]++; dd @eaten.kv; # (Noms::Walkingstuff, Any, Noms::Greenstuff, Any, Noms::Syntetics, 1).Seq
Perl 6 gets use closer to correct programs, one key at a time.
UPDATE: With commit/fef3655 Array shapes understand enums directly, the prefix:<+> is therefor not required anymore. (That’s less then 24h reaction time on a bug that wasn’t even reported :)