9

I'm having trouble understanding what is wrong with this Raku code.

I want to fetch JSON from a website, and print out a field from each item in an array within the JSON (in this case the titles of latest topics from any Discourse forum).

This is code that I expected to work, but it failed:

use HTTP::UserAgent; use JSON::Tiny; my $client = HTTP::UserAgent.new; $client.timeout = 10; my $url = 'https://meta.discourse.org/latest.json'; my $resp = $client.get($url); my %data = from-json($resp.content); # I think the problem starts here. my @topics = %data<topic_list><topics>; say @topics.WHAT; #=> (Array) for @topics -> $topic { say $topic<fancy_title>; } 

The error message is from the say $topic<fancy_title> line:

Type Array does not support associative indexing. in block <unit> at http-clients/http.raku line 18 

I would have expected that $topic should be written as %topic, because it's an array of hashes, but this doesn't work:

for @topics -> %topic { say %topic<fancy_title>; } 

The error message for that is:

Type check failed in binding to parameter '%topic'; expected Associative but got Array ([{:archetype("regula...) in block <unit> at http-clients/http.raku line 17 

If you inspect the data, it should be a hash, not an array. I tried @array but I know that isn't correct, so I changed %topic to $topic.

I finally got it to work by adding .list to the line that defines @topics but I don't understand why that fixes it, because @topics is an (Array) whether that is added or not.

This is the working code:

use HTTP::UserAgent; use JSON::Tiny; my $client = HTTP::UserAgent.new; $client.timeout = 10; my $url = 'https://meta.discourse.org/latest.json'; my $resp = $client.get($url); my %data = from-json($resp.content); # Adding `.list` here makes it work, but the type doesn't change. # Why is `.list` needed? my @topics = %data<topic_list><topics>.list; say @topics.WHAT; #=> (Array) # Why is it `$topic` instead of `%topic`? for @topics -> $topic { say $topic<fancy_title>; } 

Does anyone know why it's failing and the correct way to perform this task?

1
  • Re: Why is it $topic instead of %topic? , I tried %topic in the same position and got the same result. Commented Apr 24, 2023 at 13:41

1 Answer 1

10

What's happened is that you've created an array of an array when you say

my @topics = %data<topic_list><topics>; 

This isn't unique to these modules, but general across Raku with array assignments.

Let's take a simpler hash to see what's going on:

my %x = y => [1,2,3]; my $b = %x<y>; my @b = %x<y>; say $b; # [1 2 3] say @b; # [[1 2 3]] 

The catch is that the array assignment (which is used when the variable has the @ sigil) interprets %x<y> as a single item as it's in a scalar container, which it then happily puts in @b[0]. While you can't control the module itself, you can see the difference in my example if you say my %x is Map = … as Map do not place items in scalar containers, but Hash objects do. There are two ways to tell Raku to treat the single item as its contents, rather than a single container.

  • Bind the array
    Instead of using @b = %x<y>, you use @b := %x<y>. Binding to @-sigiled variables decontainerizes automatically.
  • Use a zen operator
    When you want to avoid the possibility of a list/hash value being treated as one, you can use a zen slice to remove any container if it happens to be there. This can be done either at assignment (@b = %x<y>[]) or at the for loop (for @b[] -> $b). Note that <>, [], and {} are effectively synonymous, regardless the actual type — most people just use the one that matches the previous.

So in your code, you could just do:

... my %data = from-json($resp.content); my @topics := %data<topic_list><topics>; # (option 1) binding my @topics = %data<topic_list><topics><>; # (option 2) zen slice for @topics -> $topic { say $topic<fancy_title>; } 

Or in your loop, as option 3:

for @topics<> -> $topic { say $topic<fancy_title>; } 

The reason that the .list fixes things — as you can probably surmise after the rest of the answer — is that it returns a fresh list that isn't in a container.

Sign up to request clarification or add additional context in comments.

7 Comments

Thanks. I actually did do something like %data<topic_list><topics>[] at one point due to a typo (must have not pressed 0 hard enough) and it worked, but I thought that it was an accidental quirk of the language that probably wasn't the right way to do things. :) I'll experiment with both of the methods you mentioned.
[] is empty, yet returns everything, hence the name zen slice (that and it kind of looks like two hands in meditation to me). The issue you've encountered, btw, is why I personally try to have my modules return Map and List objects whenever possible instead of Hash or Array, since the latter ones containerize their values.
"There are two ways to tell Raku to treat the single item as its contents, rather than a single container." Aren't there loads of them? Aiui the essential issue is the ambiguity between the dual plural / singular nature of any plural thing. Is an Array a single thing (an array) or a list of things (the contents of the array)? If a value is in a Scalar then Raku presumes you want to treat whatever is in the Scalar as a single thing. But if your code (not just your mind!) treats it as a plural thing, then you get the contents. Thus |, .list, flat, et al, will all work, right?
I haven't learned Map and List yet, but I'll research them. I've only looked through raku.guide but just found this page and will read it docs.raku.org/language/list
@raiph decont is necessary if you're doing directly to the for loop in some cases. I know I've run into it once or twice where I had to use for foo.bar(…)<> { … }. But honestly, using some of other techniques can make code more explicit, particularly the .list
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.