Archive
Dynamic declaration
Shortly after my last blog post, Stashes raised a question. Coincidence? Conspiracy? You decide! Anyway, the EVAL
caught my eye, because with it we can dynamically create compile time constructs such as a package
.
our package EXPORTHOW {
}
sub EXPORT($declarator-name = 'registered') {
use MONKEY-SEE-NO-EVAL;
OUR::EXPORTHOW::DECLARE := EVAL q:s:to /EOH/;
package DECLARE {
constant $declarator-name = MetamodelX::RegisteredHOW;
}
EOH
Map.new
}
Thanks to be our
-scoped, the package EXPORTHOW
can be modified at runtime. The EVAL
allows to define a constant
at runtime. Runtime in this context, is when the use
statement of a consuming module is executed.
use Registered 'remembered', :recall-types;
remembered Foo {
method answer { 42 }
method common { self.^name }
}
With this technique, we allow the user of a module to decide what symbols are being used for a declarator. Pretty handy, if a module is added late to a project, which might have occupied a given symbol already. Quite some time ago, I lamented a little lizmats decision to modify class
with InterceptAllMethods
. This is now a solvable problem.
I once believed Raku to be less dynamic then Perl. Looks like I have to reconsider.
Most fancy
On Discord (yes, we are that cool) MrDispatch wished for a way to collect a bunch of classes right after their declaration. I believe, with the power of the MOP, we can go a step further and register the type-object even before the definition is finished.
class MetamodelX::RegisteredHOW is Metamodel::ClassHOW {
our @registered;
method new_type(|) {
my \type = callsame;
type.HOW.remember_type(type);
type
}
method remember_type(Mu \type) {
@registered.push: type
}
method recall-types {
@registered;
}
}
sub recall-types is export(:recall-types) {
MetamodelX::RegisteredHOW.recall-types;
}
my package EXPORTHOW {
package DECLARE {
constant registered = MetamodelX::RegisteredHOW;
}
}
We introduce a new declarator registered
that hooks up a new meta-class. It would be possible to overload class
to register all classes in a compilation unit. For now, doing so would not be good conduct, because playing with EXPORTHOW
is not quite lexical yet. The list of declared type objects will reside as a class-attribute inside the meta-class. We can access it through any type object or instance via .HOW
or with the exported sub recall-types
.
use v6.*;
use Registered :recall-types;
registered Foo {
method answer { 42 }
method common { self.^name }
}
registered Bar {
method ohai { ‚Hello Universe!‘ }
method common { self.^name }
}
Foo.HOW.recall-types.say;
say Foo.new.HOW.recall-types;
say recall-types; # requires the import adverb :recall-types;
say recall-types».common;
# OUTPUT: [(Foo) (Bar)]
# [(Foo) (Bar)]
# [(Foo) (Bar)]
# [Foo Bar]
There are plenty of other ways to implement a registry of types. Using a custom meta-class is the most fancy and flexible way. We could hook into many other things here too. Rakudo is a dynamic compiler for a dynamic language. It uses introspection much more then you do. Sometimes it introspects a package called EXPORTHOW for a key called DECLARE to create a keyword that is more classy then class
.
Inequality
As stated before, I like to read code I didn’t write myself. Flavio had trouble with triples. One line stood out to me.
take @triple if $N == @triple.any;
This looks like a set-operation to me. But using $N ∈ @triple
dropped one result. After some debugging I found the culprit.
.map({($_, $n / $_)}); # take it and its counterpart
This might look like a division but is actually a type cast to Rat
.
say 1 ∈ (1/1, );
# OUTPUT: False
Rakudo implements set-operations as equivalence checks, not numerical equality. Quite in contrast to ==
, eqv
does a type check and Int
aint’t Rat
. This might explain that the mathematical inclined don’t use Set
as much as I did expect them to. The type mismatch simply produces a result that is not useful to mathematicians.
As implemented right now, we can’t tell the set operators what we consider equality . With meta-operators and junctions, we can specify what operator we actually want to use. The set-operators are not meta-operators and at least for now, we can’t user define new meta-operators. However, we can lexically redefine ordinary operators.
proto sub infix:<(elem)>($, $, *% --> Bool:D) is pure {*}
multi sub infix:<(elem)>(Numeric:D \a, Iterable:D \listy --> Bool:D) {
Any.fail-iterator-cannot-be-lazy('∈', '') if listy.is-lazy;
for listy -> \b {
return True if a == b;
}
False
}
constant &infix:<∈> := &infix:<(elem)>;
proto sub infix:<(&)>(|) is pure {*}
multi sub infix:<(&)>(Iterable:D \lhs, Iterable:D \rhs) {
Any.fail-iterator-cannot-be-lazy('∩', '') if lhs.is-lazy || rhs.is-lazy;
my @result;
for lhs -> \l {
for rhs -> \r {
@result.push: r if l == r;
}
}
+@result ?? @result !! ∅
}
constant &infix:<∩> := &infix:<(&)>;
say 1 ∈ (1/1, );
say (42, 42/2, 42/3) ∩ (1, 21, 3);
# OUTPUT: True
# Set(21)
The proto
is needed, because we need to get rid of multi-candidates that are already present. A more general approach might be to have a &*SET-COMPARATOR
that defaults to &infix:<cmp>
. This would slow down a fairly simple operator by a factor of 13. I may still be able to write a module that takes a comparator and returns a host of operators. With proper macros this would be easy. Maybe next year.
For now I shall be evil and report back with success.
They returned an empty package
I don’t like to solve maths-puzzles. I do like to read other folks solutions thought. You never know where to spot a new idiom. A good way to find them is to look for code that feels unusual.
method normalize (Numeric:D $sum = 1) {
my $total = self.total or return;
my $factor = $sum / $total;
%!pmf.values »*=» $factor;
self;
}
I have never seen the construct in the first line, at least not in a method. The return
is triggered when self.total
returns something falsesy, like 0. It protects the 2nd line from a division by zero by returning Nil
. Let’s see if that actually works.
$cookie.multiply('Bowl 1', 0);
$cookie.multiply('Bowl 2', 0);
say 'probability it came from Bowl 1: ', $cookie.P('Bowl 1');
# OUTPUT: Attempt to divide by zero when coercing Rational to Str
in sub MAIN at tbr-pmf.rakumod line 73
in block <unit> at tbr-pmf.rakumod line 3
Well, it does blow up some place else. This is not an unreasonable scenario either. When I’m around, the likelihood of a cookie to come from bowl1 or bowl2 is indeed 0. Returning to normalize
we can check what happens when a method that should return self
returns Nil
.
class C {
method foo { Nil }
}
dd C.new.foo.bar;
# OUTPUT: Nil
The cause of this mis-dispatch can be found in src/core.c/Nil.pm6:16:
method FALLBACK(| --> Nil) { }
Nil
is the baseclass of Failure
and as such will only throw when assigned to or used to gain values from a list. It’s purpose is to revert containers to their default value. In a numeric context it will warn and turn into 0. Eventually it might end up in a division and cause much grief. Dealing with a depressing lack of cookies could be done with a multi-method.
multi method P ($key where { self.total == 0 } ) {
0
}
multi method P ($key) {
die "no key '$key' in PMF" unless %!pmf{$key}:exists;
return %!pmf{$key} / self.total;
}
This works because self
is an implicit part of the signature of a method (sans an explicit invocant).
Another thing I never seen before is the following gist
method.
method gist () {
return gather {
take '---';
for %!pmf.keys.sort -> $key {
take " «$key» {%!pmf{$key}}";
}
}.join("\n");
}
Using take
to prefix the resulting list is quite neat. Using a gather
-block avoids any nesting that might need flattening afterwards. In a gist
-method it’s a bit wasteful though. We like to truncate lists after 100 elements in say
. This could be done with a .head(100)
after .keys
. Or we skip the rather slow gather
-block altogether (each take
fires a control exception and Rakudo can not optimise those away yet).
method gist () {
( |'---', |%!pmf.keys.sort.map: { " «$_» %!pmf{$_}" } ).head(100).join($?NL)
}
I like to avoid explicit flattening by using Slip
s. If the map
-block gets complicated, this allows to sneak a .hyper
in to gain some speed on large lists.
Please keep in mind that cookies are nice and Nil
is shifty.
Only infinite elements
Flavio solved a puzzle with an implementation that puzzled me. The following code appears not as idiomatic as such a simple problem should require.
sub simulation-round () {
return [+] gather {
loop {
my $value = roll-die();
take $value;
last if $value < 3;
}
}
}
We tend to do our dice rolls with a simpler construct.
sub simulation-round () {
[+] (1..6).roll(*).map({ last .Int if .Int < 3; .Int });
}
# OUTPUT: Cannot .sum a lazy list onto a Seq
# in sub simulation-round at ETOOBUSY-2.raku line 4
# in block <unit> at ETOOBUSY-2.raku line 7
Rakudo assumes that a lazy list must be infinite. This would catch many bugs but is not what I want in this case. Sadly we don’t have a build-in to say .lazy-but-finite
. Neither do we got roles we could mixin. This might be the reason why it took me 10 minutes of staring‑at‑the‑code to find a solution.
[+] (1..6).roll(*).map({ last if .Int < 3; .Int })[^∞]
So we are asking for up to infinite elements of this infinite list to defuse the laziness. This smells of bad design and may warrant a problem solving issue.
The rest of my version is quite different because I wanted to shoehorn a .hyper
in.
use v6.*;
unit sub MAIN($rounds = 1_000_000);
sub term:<🎲>() { (1..6).roll(*) };
sub simulation-round () {
[+] 🎲.map({ last .Int if .Int < 3; .Int })[^∞];
}
my $total = (0..^$rounds).hyper(:degree(12), :batch(10000)).map({ simulation-round }).sum;
put ‚average gain: ‘, $total / $rounds;
I believe there is a life‑lesson to be learned. Laziness can be problematic if the compiler is less then virtuos.
UPDATE:
As lizmat pointed out, last .Int
requires v6.e.PREVIEW
(v6.*
will work too). Instead of [^∞]
a simple .eager
will do (but will break the title of the blog post). We can also use ^$rounds
instead of (0..^$rounds)
if we disambiguate with a space before the method call.
my $total = ^$rounds .hyper(:degree(12), :batch(10000)).map({ simulation-round }).sum;