5

I'm trying to make a Hash with non-string keys, in my case arrays or lists.

> my %sum := :{(1, 3, 5) => 9, (2, 4, 6) => 12}
{(1 3 5) => 9, (2 4 6) => 12}

Now, I don't understand the following.

How to retrieve an existing element?

> %sum{(1, 3, 5)}
((Any) (Any) (Any))

> %sum{1, 3, 5}
((Any) (Any) (Any))

How to add a new element?

> %sum{2, 4} = 6
(6 (Any))
Elizabeth Mattijsen
  • 25,654
  • 3
  • 75
  • 105
Eugene Barsky
  • 5,780
  • 3
  • 17
  • 40
  • 2
    I wrote about object hashes in last year's Advent calendar: https://perl6advent.wordpress.com/2016/12/03/day-3-object-hashes/ – brian d foy Dec 22 '17 at 20:05
  • @briandfoy Thank you, I'll read it with great pleasure! And I'm looking forward to reading your book as soon as it appears. :) – Eugene Barsky Dec 22 '17 at 22:32

2 Answers2

7

Several things are going on here: first of all, if you use (1,2,3) as a key, Rakudo Perl 6 will consider this to be a slice of 3 keys: 1, 2 and 3. Since neither of these exist in the object hash, you get ((Any) (Any) (Any)).

So you need to indicate that you want the list to be seen as single key of which you want the value. You can do this with $(), so %sum{$(1,3,5)}. This however does not give you the intended result. The reason behind that is the following:

> say (1,2,3).WHICH eq (1,2,3).WHICH
False

Object hashes internally key the object to its .WHICH value. At the moment, Lists are not considered value types, so each List has a different .WHICH. Which makes them unfit to be used as keys in object hashes, or in other cases where they are used by default (e.g. .unique and Sets, Bags and Mixes).

I'm actually working on making this the above eq return True before long: this should make it to the 2018.01 compiler release, on which also a Rakudo Star release will be based.

BTW, any time you're using object hashes and integer values, you will probably be better of using Bags. Alas not yet in this case either for the above reason.

You could actually make this work by using augment class List and adding a .WHICH method on that, but I would recommend against that as it will interfere with any future fixes.

Elizabeth Mattijsen
  • 25,654
  • 3
  • 75
  • 105
  • 1
    Thanks! So for now there is no way to do it, but in future it will be possible? Then probably the best way for me will be to make strings out of these lists... – Eugene Barsky Dec 22 '17 at 13:21
  • Yeah, that would be the solution atm. – Elizabeth Mattijsen Dec 22 '17 at 21:02
  • @ElizabethMattijsen You indicated on your recent blog post that one could override `.WHICH` to make objects work as value-type hash keys. Is there any reason he shouldn't do that here? – piojo Dec 23 '17 at 07:00
  • `class Key { has Int @.list handles ; method WHICH() { ObjAt.new(@.list.join('|')); } }` seems to work for me. I'm gonna add it as an answer, and you can shoot me down if there's anything I missed. – piojo Dec 23 '17 at 07:06
  • 1
    As of https://github.com/rakudo/rakudo/commit/6cca27669f `List`s consisting of just value types, will be a value type. I would not yet depend on this feature until 2018.01, in case the commit gets reverted. – Elizabeth Mattijsen Dec 23 '17 at 13:04
5

Elizabeth's answer is solid, but until that feature is created, I don't see why you can't create a Key class to use as the hash key, which will have an explicit hash function which is based on its values rather than its location in memory. This hash function, used for both placement in the list and equality testing, is .WHICH. This function must return an ObjAt object, which is basically just a string.

class Key does Positional {
  has Int @.list handles <elems AT-POS EXISTS-POS ASSIGN-POS BIND-POS push>;
  method new(*@list) { self.bless(:@list); }
  method WHICH() { ObjAt.new(@!list.join('|')); }
}

my %hsh{Key};
%hsh{Key.new(1, 3)} = 'result';
say %hsh{Key.new(1, 3)}; # output: result

Note that I only allowed the key to contain Int. This is an easy way of being fairly confident no element's string value contains the '|' character, which could make two keys look the same despite having different elements. However, this is not hardened against naughty users--4 but role :: { method Str() { '|' } } is an Int that stringifies to the illegal value. You can make the code stronger if you use .WHICH recursively, but I'll leave that as an exercise.

This Key class is also a little fancier than you strictly need. It would be enough to have a @.list member and define .WHICH. I defined AT-POS and friends so the Key can be indexed, pushed to, and otherwise treated as an Array.

Elizabeth Mattijsen
  • 25,654
  • 3
  • 75
  • 105
piojo
  • 6,351
  • 1
  • 26
  • 36
  • It works! But if I give it an array as an argument and its sum as a value, the output is a 1-element list, not an `Int`. `my @lst = 5 .. 10; my @lst_k = Key.new(@lst); %hsh{@lst_k} = [+] @lst; say [+] @lst; say %hsh{@lst_k}; # (45)`. – Eugene Barsky Dec 23 '17 at 09:47
  • 1
    @EugeneBarsky The main reason that doesn't work is that you're storing the `Key` (which isn't a list) in a @-sigiled variable, which makes it into a one-element array. I've updated my answer to make `Key` do `Positional`, so you can put it in that variable if you want. But you must use `:=` for the assignment, or you'll get into the same problem (with a `Key` as an array's only element). In other words: `my @lst_k := Key.new(@lst);`. But since this class probably isn't a perfect array/list implementation, I suggest using a `$` variable so as to think of it as a key, not a normal array. – piojo Dec 23 '17 at 09:55
  • 2
    As of https://github.com/rakudo/rakudo/commit/202459ce0b9cf0 there is a `ValueObjAt` object that indicates that it is an `ObjAt` that is a value type. So your `method WHICH` should be changed to `ValueObjAt.new(@!list.join('|'))` to make sure it is recognized as a value type everywhere. Please note that `ValueObjAt` is just an empty subclass of `ObjAt` to allow for easier value typeness checking. – Elizabeth Mattijsen Dec 23 '17 at 13:09
  • @ElizabethMattijsen Thanks. I'll add that to the answer later, after we can assume most people are running an interpreter that has that class. – piojo Dec 25 '17 at 09:19