Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Extracting JSON from a Raku HTTP client request

Tags:

raku

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?

like image 937
R891 Avatar asked Nov 25 '20 23:11

R891


1 Answers

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.

like image 150
user0721090601 Avatar answered Nov 04 '22 18:11

user0721090601