Reputation: 165
I'm trying to create a Perl 6 client interface to an RPC server, with the class hierarchy matching the server URLs. e.g.
# for url path: /account/login
# client code:
$client.account.login;
For this to work, the 'child' object (account) needs to store a reference to its parent object (client).
This is what I've tried:
#!/usr/bin/env perl6
use v6;
class MyApp::Client::Account {
has $!client;
method login() {
# fake login
$!client.session_id = 'abc';
return 'ok';
}
}
class MyApp::Client {
has $.session_id is rw;
method account() {
state $empire = MyApp::Client::Account.new( :client(self) );
return $empire;
}
}
use Test;
plan( 2 );
my $client = MyApp::Client.new;
my $response = $client.account.login;
is( $response, 'ok', 'login successful' );
ok( $client.session_id, 'client has session_id' );
Running this gives the following error message:
1..2
Method 'session_id' not found for invocant of class 'Any'
in method login at test.pl6 line 9
in block <unit> at test.pl6 line 29
# Looks like you planned 2 tests, but ran 0
I don't really know any perl6 class/object idioms yet - am I even going about the design in the right way?
If so, why is $!client
within the login()
method undefined?
For reference, here's the perl5 (bare-bones) version that I'm trying to convert from:
#!/usr/bin/env perl
package MyApp::Client::Account;
sub new {
my $class = shift;
return bless {@_}, $class;
}
sub login {
my $self = shift;
# fake login
$self->{client}->session_id( 'abc' );
return 'ok';
}
package MyApp::Client;
sub new {
my $class = shift;
return bless {@_}, $class;
}
sub session_id {
my $self = shift;
if (@_) {
$self->{session_id} = shift;
}
return $self->{session_id};
}
sub account {
my $self = shift;
$self->{account} ||= MyApp::Client::Account->new( client => $self );
return $self->{account};
}
package main;
use Test::More tests => 2;
my $client = MyApp::Client->new;
my $response = $client->account->login;
is( $response, 'ok', 'login successful' );
ok( $client->session_id, 'client has session_id' );
Which gives the expected output:
1..2
ok 1 - login successful
ok 2 - client has session_id
Upvotes: 3
Views: 115
Reputation: 1108
So there are a few ways that Perl 6 OO differs from other implementations I've used. One is the awesome way that it will auto-fill your member variables for you. However, this only works when they are defined with public accessors.
class G {
has $!g;
has $.e;
method emit { say (defined $!g) ?? "$!g G thing" !! "nada G thing" }
}
Which will lead to the following behavior:
> my $g = G.new( :g('unit-of-some-kind'), :e('electric') )
G.new(e => "electric")
> $g.emit
nada G thing
> $g.e
electric
So when you are passing self
as a reference to MyApp::Client::Account
, it isn't being bound to the $!client
variable because the default constructor will only bind to publicly accessible member variables.
You can either choose to make it accessible, or you can take the object construction logic into your own hands. This is how I imagine the code to look were I to need my own version in Perl 6 but had to keep client private:
class MyApp::Client::Account {
has $!client;
method new(:$client) {
self.bless( :$client );
}
# binds $!client to $client automatically based on the signature
submethod BUILD(:$!client) { }
method login() {
# fake login
$!client.session_id = 'abc';
return 'ok';
}
}
class MyApp::Client {
has $.session_id is rw;
has $.account;
# the default .new will call .bless for us, which will run this BUILD
submethod BUILD {
$!account = MyApp::Client::Account.new( :client(self) );
}
}
It can take some getting used to the new
versus BUILD
distinction. One key distinguishing point is that self
is not available in the scope of new
, but it is available in the scope of BUILD
(albeit in a not-yet-fully-constructed form).
Upvotes: 5