Mapping a List with a Hash argument

161 Views Asked by At

In the docs for class List, it says:

routine map

multi method map(Hash:D \hash)  
multi method map(Iterable:D \iterable)   
multi method map(|c) 
multi method map(\SELF: █; :$label, :$item)   multi sub map(&code, +values)
multi sub map(&code, +values)

In the first multi method clause, I think the signature says that the method takes one argument, which is an instance of the Hash class (Hash:D). The argument will be assigned to a sigil-less variable \hash (which is a constant), which means you know the hash in the caller cannot be changed by the method.

Based on the method name, map, and the hash argument, it seems likely that the hash will map the elements of a list to the corresponding values in the hash. Let's try it:

[106] > my %hash = %{1 => 'apple', 2 => 'tree', 3 => 'octopus'};
{1 => apple, 2 => tree, 3 => octopus}

[107] > (1, 2, 3).map(%hash);
Cannot map a List using a Hash
Did you mean to add a stub ({ ... }) or did you mean to .classify?
  in block <unit> at <unknown file> line 1
  in any <main> at /usr/local/bin/rakudo-moar-2024.01-01-macos-arm64-clang/bin/../share/perl6/runtime/perl6.moarvm line 1
  in any <entry> at /usr/local/bin/rakudo-moar-2024.01-01-macos-arm64-clang/bin/../share/perl6/runtime/perl6.moarvm line 1

[107] > 

Cannot map a List using a Hash. Whaaa?! How can I call $list.map(%some_hash)?

3

There are 3 best solutions below

3
Silvio Mayolo On BEST ANSWER

This looks like a case where Raku tried to be incredibly helpful and it backfired. The relevant overloads for Raku's map are defined here.

proto method map(|) is nodal {*}
multi method map(Hash:D \hash) {
    X::Cannot::Map.new(
      what       => self.^name,
      using      => "a {hash.^name}",
      suggestion =>
"Did you mean to add a stub (\{ ... \}) or did you mean to .classify?"
    ).throw;
}
multi method map(Iterable:D \iterable) {
    X::Cannot::Map.new(
      what       => self.^name,
      using      => "a {iterable.^name}",
      suggestion =>
"Did a * (Whatever) get absorbed by a comma, range, series, or list repetition?
Consider using a block if any of these are necessary for your mapping code."
    ).throw;
}
multi method map(|c) {
    X::Cannot::Map.new(
      what       => self.^name,
      using      => "'{c.raku.substr(2).chop}'",
      suggestion => "Did a * (Whatever) get absorbed by a list?"
    ).throw;
}

That is, all of the overloads for map except the one taking a &code object just produce specific, helpful error messages like the one you saw. Unfortunately, those overloads got picked up by some sort of (presumably) automated documentation tool and added to the docs, creating more confusion.

It seems to be that these error-only overloads should probably be hidden from the documentation, to prevent exactly this sort of thing from tripping folks up.

0
raiph On

TL;DR What @SilvioMayolo++ said. Alternatively...

The map function is about applying a function to each element of an ordered list. A hash doesn't really fit in that framework. But I'll try make it fit.

One plausible answer to one (loose) interpretation of your question is:

my %hash = %{ 1 => 'apple', 2 => 'tree', 3 => 'octopus' };
say (1, 2, 3).map: { %hash{$_} } # (apple tree octopus)

If that's not what you meant, then perhaps the suggestion in the error message was on the money:

Did you mean to add a stub ({ ... }) or did you mean to .classify?

You obviously didn't mean to add a stub ({...}) but maybe you meant .classify?:

say (1, 2, 3).classify(%hash);      # {apple => [1], octopus => [3], tree => [2]}
say (1, 2, 3).classify(%hash)>>[0]; # {apple => 1, octopus => 3, tree => 2}

If none of the above are what you meant, then please clarify in a comment.

0
jubilatious1 On

What @raiph said.

But maybe you want :p pairs back?

my %hash = %{ 1 => 'apple', 2 => 'tree', 3 => 'octopus' };
say (1,2,3).map(-> $x { %hash{$x}:p  });

#Returns:

(1 => apple 2 => tree 3 => octopus)

Scrambled list:

my %hash = %{ 1 => 'apple', 2 => 'tree', 3 => 'octopus' };
say (3,1,2).map(-> $x { %hash{$x}:p  });

#Returns:

(3 => octopus 1 => apple 2 => tree)

Note: if you try the .pairs routine, you'll return the index-position of the List elements instead. Essentially the map call replaces the %hash keys with the zero-indexed ordinal, in a key/value pair:

my %hash = %{ 1 => 'apple', 2 => 'tree', 3 => 'octopus' };

say (1,2,3).map(-> $x { %hash{$x} }).pairs;
#Returns:
(0 => apple 1 => tree 2 => octopus)

say (3,1,2).map(-> $x { %hash{$x} }).pairs;
#Returns:
(0 => octopus 1 => apple 2 => tree)

(Above is very useful for re-ordering a list of elements according to a given List "index". Imagine calling .values instead of .pairs at the end).

https://docs.raku.org/type/Hash#class_Hash