Home > Raku > The truth is a hard problem

The truth is a hard problem

In a recent article, stevied promised a detailed walk through of code. I asked him if he would be interested in a critique of his writings. He foolishly agreed.

He claims that Arrays are eager and fill all memory if given the chance. Let’s check this claim.

my @a = 1, 1, * + * … ∞;
say @a.WHAT;

# OUTPUT: (Array)

Array.STORE goes out of it’s way to maintain iterables (not all things that are iterable do Iterable) and only reify container slots that we actively ask for. It will cache lazy lists by default, assuming that when we use .AT-POS (usually indirectly with a positional subscript) we want to use the values again. Dealing with lazy lists is a hard problem and many get it wrong (as my next blog post will discuss in more detail).

In the next paragraph I found another inaccurate statement. Int:D does not check for definedness.

multi sub bar(Any:D $) { say 'defined'; }
multi sub bar(Any:U $) { say 'undefined'; }

bar(Int); # undefined

class C { method defined { False } }
my $c = C.new; # undefined
bar(C); # undefined
bar($c); # defined
say defined $c; # False
say $c.DEFINITE; # True
say $c // 'undefined'; # undefined

In Raku objects are either type objects or definite objects. The latter are the result of nqp::create. Raku and most user code expects objects to provide a basic set of methods that can be found in Mu or Any. The default type-check is against Mu. We can check if an object is definite with the macro .DEFINITE (you can overload a method of the same name, but Rakudo will ignore it) or, if we also want to know if we got an ordinary object, with $foo ~~ Mu:D. There is a clear distinction of definedness and being definite. Raku needs to make that differentiation to support “unusual” values and types like Nil and Failure. Please note, that type-objects are not definite but can be defined. Types are a hard problem.

In the same paragraph stevied writes: “defined integer object”, even though he makes a type-check against Int.

multi sub foo(Int:D $i) { }
multi sub foo($i where { $i - $i.truncate == 0 }) { say 'lolwut‽' }
multi sub foo($) { say 'not integer'; }

foo(1.0);
foo(1/1);
foo(¼);

# OUTPUT: lolwut‽
          lolwut‽
          not integer

Int:D will fail for any value that doesn’t got (Int) in its .^mro or is a subset of Int or any of Int‘s sub-classes. Raku sports an excellent coercion protocol and there is no reason not use it.

subset PositiveInteger of Numeric:D() where { $^i > 0 && ($i - $i.truncate == 0) || fail("Expected a positive and an integer value but got $i.")}
sub get-prime(PositiveInteger $nth where * > 0) {
    say ($x.grep: *.is-prime)[$nth - 1];
}
get-prime('5');
get-prime(½);
# OUTPUT: 11
          Expected a positive and an integer value but got 0.5.
            in block  at 2021-03-08.raku line 1809
            in block <unit> at 2021-03-08.raku line 1288

A value check is needed in this instance because .AT-POS will coerce to Int (by truncating) what may give a reasonable answer for an unreasonable question. No matter how sharp the knife, we wont get the ½th prime number. Being positive and integer is a hard problem.

Please don’t use say unless you actually want to run .gist.

say Nil;
put Nil;
# OUTOUT: Nil
          Use of Nil in string context
            in block  at 2021-03-08.raku line 1818

When you say you may miss subtile bugs, especially when running CI-tests.

Further down we find Sequence but grep returns a Seq – in this example. I never had to make the distinction between Seq and HyperSeq but the latter is not a child of Cool so a lot of interfaces are missing. The same is true for the role Sequence. The build-in types are a hard problem.

If we must ask what exactly * is (I wouldn’t, because that’s a really hard question.), it is best to provide the correct answer.

my &b = * + *;
say .WHAT, .signature with &b;
my &b2 = { $^a + $^b };
say .WHAT, .signature with &b2;
# OUTPUT: (WhateverCode)(;; $whatevercode_arg_11 is raw, $whatevercode_arg_12 is raw)
          (Block)($a, $b)

A WhateverCode-object will have all its arguments as is raw, what means it will always have access to the containers used by the callee (There are many things we can do, but probably shouldn’t, with .VAR). It will never have control exceptions (no return or phasers, etc.), doesn’t have a proper scope (no use-statement) and a few more things. The idea is to have a subclass of Code that is so simple that the compiler can always inline it. Code-objects are a hard problem.

I would have written that code a little different.

sub get-primes(*@nth) {
    (^∞).hyper.grep(-> Int:D() $_ is raw { .is-prime })[@nth »-» 1]
}

.put for get-primes(5, 50, 500, 5000, 50000);

I spend a pretty penny on that Threadripper, so I better .hyper as often as I can. If sensible I try to use a slurpy to move the burden of dealing with plurals from the user to the implementer. We can ask .AT-POS for a list of values, so there is no reason not to.

There are quite a few more inaccuracies in Steve’s article. Bless him, Raku is a bitch and we are not good at explaining how the language actually works. For most programs that doesn’t matter, they will run just fine even if the programmer is sloppy. I learned most of the gritty details when tracking down bugs. Granted, I stick my arm deep into the machinery, so it’s my own bloody fault when I get pinched. Since bugs happen we need to get better at explaining how to hunt them down. Raku is a hard problem.

Advertisement
Categories: Raku
  1. No comments yet.
  1. July 11, 2022 at 14:59
  2. July 22, 2022 at 21:05

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: