fireartist
fireartist

Reputation: 165

How to store a reference to parent object in perl6 (conversion from perl5)

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

Answers (1)

ab5tract
ab5tract

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

Related Questions