Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use hyperoperators with Scalars that aren't really scalar?

Tags:

raku

rakudo

I want to make a hash of sets. Well, SetHashes, since they need to be mutable.

In fact, I would like to initialize my Hash with multiple identical copies of the same SetHash.

I have an array containing the keys for the new hash: @keys

And I have my SetHash already initialized in a scalar variable: $set

I'm looking for a clean way to initialize the hash.

This works:

my %hash = ({ $_ => $set.clone } for @keys);

(The parens are needed for precedence; without them, the assignment to %hash is part of the body of the for loop. I could change it to a non-postfix for loop or make any of several other minor changes to get the same result in a slightly different way, but that's not what I'm interested in here.)

Instead, I was kind of hoping I could use one of Raku's nifty hyper-operators, maybe like this:

my %hash = @keys »=>» $set;

That expression works a treat when $set is a simple string or number, but a SetHash?

Array >>=>>> SetHash can never work reliably: order of keys in SetHash is indeterminate

Good to know, but I don't want it to hyper over the RHS, in any order. That's why I used the right-pointing version of the hyperop: so it would instead replicate the RHS as needed to match it up to the LHS. In this sort of expression, is there any way to say "Yo, Raku, treat this as a scalar. No, really."?

I tried an explicit Scalar wrapper (which would make the values harder to get at, but it was an experiment):

my %map = @keys »=>» $($set,)

And that got me this message:

Lists on either side of non-dwimmy hyperop of infix:«=>» are not of the same length while recursing
left: 1 elements, right: 4 elements

So it has apparently recursed into the list on the left and found a single key and is trying to map it to a set on the right which has 4 elements. Which is what I want - the key mapped to the set. But instead it's mapping it to the elements of the set, and the hyperoperator is pointing the wrong way for that combination of sizes.

So why is it recursing on the right at all? I thought a Scalar container would prevent that. The documentation says it prevents flattening; how is this recursion not flattening? What's the distinction being drawn?

The error message says the version of the hyperoperator I'm using is "non-dwimmy", which may explain why it's not in fact doing what I mean, but is there maybe an even-less-dwimmy version that lets me be even more explicit? I still haven't gotten my brain aligned well enough with the way Raku works for it to be able to tell WIM reliably.

like image 934
Mark Reed Avatar asked Dec 16 '20 18:12

Mark Reed


1 Answers

I'm looking for a clean way to initialize the hash.

One idiomatic option:

my %hash = @keys X=> $set;

See X metaoperator.


The documentation says ... a Scalar container ... prevents flattening; how is this recursion not flattening? What's the distinction being drawn?

A cat is an animal, but an animal is not necessarily a cat. Flattening may act recursively, but some operations that act recursively don't flatten. Recursive flattening stops if it sees a Scalar. But hyperoperation isn't flattening. I get where you're coming from, but this is not the real problem, or at least not a solution.


I had thought that hyperoperation had two tests controlling recursing:

  • Is it hyperoperating a nodal operation (eg .elems)? If so, just apply it like a parallel shallow map (so don't recurse). (The current doc quite strongly implies that nodal can only be usefully applied to a method, and only a List one (or augmentation thereof) rather than any routine that might get hyperoperated. That is much more restrictive than I was expecting, and I'm sceptical of its truth.)

  • Otherwise, is a value Iterable? If so, then recurse into that value. In general the value of a Scalar automatically behaves as the value it contains, and that applies here. So Scalars won't help.


A SetHash doesn't do the Iterable role. So I think this refusal to hyperoperate with it is something else.


I just searched the source and that yields two matches in the current Rakudo source, both in the Hyper module, with this one being the specific one we're dealing with:

    multi method infix(List:D \left, Associative:D \right) {
        die "{left.^name} $.name {right.^name} can never work reliably..."
    }

For some reason hyperoperation explicitly rejects use of Associatives on either the right or left when coupled with the other side being a List value.


Having pursued the "blame" (tracking who made what changes) I arrived at the commit "Die on Associative <<op>> Iterable" which says:

This can never work due to the random order of keys in the Associative. This used to die before, but with a very LTA error about a Pair.new() not finding a suitable candidate.


Perhaps this behaviour could be refined so that the determining factor is, first, whether an operand does the Iterable role, and then if it does, and is Associative, it dies, but if it isn't, it's accepted as a single item?

A search for "can never work reliably" in GH/rakudo/rakudo issues yields zero matches.

Maybe file an issue? (Update I filed "RFC: Allow use of hyperoperators with an Associative that does not do Iterable role instead of dying with "can never work reliably".)


For now we need to find some other technique to stop a non-Iterable Associative being rejected. Here I use a Capture literal:

my %hash = @keys »=>» \($set);

This yields: {a => \(SetHash.new("b","a","c")), b => \(SetHash.new("b","a","c")), ....


Adding a custom op unwraps en passant:

sub infix:« my=> » ($lhs, $rhs) { $lhs => $rhs[0] }
my %hash = @keys »my=>» \($set);

This yields the desired outcome: {a => SetHash(a b c), b => SetHash(a b c), ....


my %hash = ({ $_ => $set.clone } for @keys);

(The parens seem to be needed so it can tell that the curlies are a block instead of a Hash literal...)

No. That particular code in curlies is a Block regardless of whether it's in parens or not.

More generally, Raku code of the form {...} in term position is almost always a Block.

For an explanation of when a {...} sequence is a Hash, and how to force it to be one, see my answer to the Raku SO Is that a Hash or a Block?.


Without the parens you've written this:

my %hash = { block of code } for @keys

which attempts to iterate @keys, running the code my %hash = { block of code } for each iteration. The code fails because you can't assign a block of code to a hash.

Putting parens around the ({ block of code } for @keys) part completely alters the meaning of the code.

Now it runs the block of code for each iteration. And it concatenates the result of each run into a list of results, each of which is a Pair generated by the code $_ => $set.clone. Then, when the for iteration has completed, that resulting list of pairs is assigned, once, to my %hash.

like image 115
raiph Avatar answered Oct 10 '22 05:10

raiph