7
my @products = (
    (name => "samsung s6" , price => "600"),
    (name => "samsung s7" , price => "700"));

# for @products -> $x { say $x{"name"} ~ ": USD" ~ $x{"price"} };

# Desired output:
#
# samsung s6: USD600
# samsung s7: USD700

# Real output: An error:
#
# "Type List does not support associative indexing."

I would also like to iterate using the .kv method and the $key and $val like so:

for @products -> $x.kv -> $key,$val  { say $x{$key} ~ " " ~ $x{$val} };

# That gives a "Malformed parameter" error

I've looked up the Raku documentation but this particular case isn't mentioned in the documentation. How to do it? Alternative ways or improvisation on the code are welcome.

yatu
  • 86,083
  • 12
  • 84
  • 139
Lars Malmsteen
  • 738
  • 4
  • 23

5 Answers5

6

It's always important to make your data structures right. In this case, they don't feel right, but I guess you are forced to use that format. In any case, I would solve it like this:

for @products.map( { |(.[0].value, .[1].value) } ) -> $name, $price {

In words: map the list with pairs to a Slip (prefix |) with just the values of the pairs, and feed that into $name and $price two elements at a time. Perhaps better readable would be:

for @products.map( { (.[0].value, .[1].value).Slip } ) -> $name, $price {
Elizabeth Mattijsen
  • 25,654
  • 3
  • 75
  • 105
  • I need to or I'd prefer to use that data structure because a problem came up which was written in Python and there the data was expressed similar to that. The `|` is also known as `flatten` operator, is that right? At the end of the day, the `map` does the trick here :) – Lars Malmsteen Aug 24 '20 at 17:52
  • The `|` is known as a [`Slip`](https://docs.raku.org/type/Slip). A `Slip` is a `List` that will automatically iterate when inserted into an iteration. But it does **not** flatten its elements. [flat](https://docs.raku.org/type/Iterable#method_flat) does. – Elizabeth Mattijsen Aug 24 '20 at 18:00
  • Liz, you could use the `Pair`-as-named-arg version of the thing you did for the prior SO about a list of `Pair`s: `for @products -> (:$name, :$price) { say "$name: USD$price" }`. – raiph Aug 24 '20 at 18:07
5

For the data given, you can use this sub-signature:

for @products -> @ ( :$name, :$price ) {
  say "$name: USD$price"
}

So this expects a single unnamed listy argument @.
It then expounds upon that by saying it contains two named values.
(This works because you have them as Pair objects.)


Really what you had was very close to working, all you had to do was coerce to a Hash.

for @products -> $x { say $x.hash{"name"} ~ ": USD" ~ $x.hash{"price"} }

for @products -> Hash() $x { say $x{"name"} ~ ": USD" ~ $x{"price"} }

Personally if I were going to do a lot of work with this data I would create an ecosystem for it.

class Product {
  has $.name;
  has $.price;

  our sub from-JSON-list ( @ (:$name, :$price) --> ::?CLASS ) {
    ::?CLASS.new( :$name, :$price )
  }

  method gist ( --> Str ) { "$.name: USD$.price" }
}


@products .= map: &Product::from-JSON-list;

.say for @products;

If you didn't like the & you could create a constant alias to the sub.

…

  constant from-JSON-list =
  our sub from-JSON-list ( @ (:$name, :$price) ) {
    ::?CLASS.new( :$name, :$price )
  }

…

@products .= map: Product::from-JSON-list;
Brad Gilbert
  • 33,846
  • 11
  • 78
  • 129
  • Nice answer. Just to mention that the raku eco system has some help for that ... https://modules.raku.org/search/?q=JSON ... personally I would lean to JSON::Class. – librasteve Aug 25 '20 at 06:25
3

Since raku OO is so lightweight, you could use a class which is a great way to manage non-uniform data structures:

class Product {
    has $.name;
    has $.price;

    method gist { "$.name: USD$.price" }
}

my @products = ( 
    Product.new(name => "samsung s6" , price => "600"),
    Product.new(name => "samsung s6" , price => "600"),
);
.say for @products;

#samsung s6: USD600
#samsung s6: USD600
librasteve
  • 6,832
  • 8
  • 30
  • Thank you for the alternative solution for the data structure. However, the code was originally written in Python and the data structure therein was written in a JSON-like notation. I had to use its counterpart in Raku and solve it using that notation. – Lars Malmsteen Aug 24 '20 at 21:20
  • Ah OK - I wasn’t sure whether to show a load() sub and see that Brad has done a good job on that below... – librasteve Aug 25 '20 at 06:29
2

Great answers already. Brad's Hash() coercion solution is so sweet!

But it felt like another answer was called for. No one directly discussed your impulse to go the key/value route using .kv, and the error you got:

I would also like to iterate using the .kv method ... "Malformed parameter"

.kv isn't helpful because of the nature of your data

Your data is such that .kv just isn't going to help you get where you need to go. (Which is why no one used it.)

Why not?

Here's your data:

    (name => "samsung s6" , price => "600"),
    (name => "samsung s7" , price => "700"),
    ...

Each element in @products is a sub-list of two key/value pairs.

But all of the keys are useless!

What you really want to do is throw away the existing keys and then construct brand new pairs by treating the value of the first pair in each @products element as a new key and the value of the second pair as the associated value.

So you end up with something like:

("samsung s6" => "600"),
("samsung s7" => "700"),

To do this you need to forget .kv.

Instead you need to suitably combine something like a .value routine with a .pairup routine. And indeed that works:

.say for @products[*;*]».value.pairup

displays:

samsung s6 => 600
samsung s7 => 700

Let's break that down:

.say for @products[*;*]

displays:

name => samsung s6
price => 600
name => samsung s7
price => 700

For an explanation of what the [*;*] is doing, see my answer to How do I “flatten” a list of lists?.

As already explained, the keys are useless. We want the value from each Pair:

.say for @products[*;*]».value

displays:

samsung s6
600
samsung s7
700

A » in this scenario is like an inner for loop, or a map. For example, I could have written:

.value.say for (do @$_ for @products[*;*])

But it's a lot easier to write just the one character ». This gets rid of the extra parens, inner for construct, and what not.

Finally we tack a .pairup on the end to pair up odd and even elements of a list; each odd element becomes a key, and the following (even) element becomes the associated value. Job done.

About the error message

The error message includes an . This is an "eject" symbol, pinpointing where the parser gave up:

------> for @products -> $x⏏.kv

The words "Malformed parameter" are calling out the . in your code:

for @products -> $x.kv ...

The $x has been parsed as a parameter, or at least the start of one. But then what's with the .? It might be invoking a method call if it were part of an expression, but here you should be finishing up specifying the name of a parameter. So the parser bails.

raiph
  • 31,607
  • 3
  • 62
  • 111
1

The reason you are getting the error is you are constructing a List of Lists of Pairs but treating it as a List of Hashes.

The simplest fix for the code is to update the data structure to match how you are treating it :

my @products = (
    {name => "samsung s6" , price => "600"},
    {name => "samsung s7" , price => "700"}
);

The {} in this case is evaluated as a Hash constructor. I think the issue arises with the fact that in Raku => is a Pair constructor as opposed to a special comma.

As others have suggested a light weight Object can be useful too but I think it's good to understand how to build what you thought you were building.

Scimon Proctor
  • 4,558
  • 23
  • 22