问题
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:
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 thefor
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.
来源:https://stackoverflow.com/questions/65014105/extracting-json-from-a-raku-http-client-request