Liksu
Liksu

Reputation: 41

How to detect that the method is in the middle of the call chain in Perl?

I have an example object with calc method:

package A;
sub new {...}
sub calc {
    my ($self, $a, $b) = @_;
    return {first => $a, second => $b, sum => $a + $b}
}

And simple usage:

my $result = A->new->calc(1, 2);
print 'Result is ', $result->{sum}; # output: Result is 3

Now, i want to add a chaining method log so that it outputs the parameters of calculation and returns result:

package A;
...
sub calc {
    ...
    return $self->{result} = {...}
}
sub log {
    my $self = shift;
    print sprintf 'a = %d, b = %d', $self->{result}->{first}, $self->{result}->{second};
    return $self->{result};
}

And use it like this cases:

my $result = A->new->calc(10, 20);
print "Result of calc: ", $result->{sum}; # output: 30

$result = A->new->calc(11, 12)->log; # output: a = 11, b = 12
print 'Result is ', $result->{sum}; # output: Result is 23

I tried to use helper object with overloads, but my calc can return very different structures like scalar, array, arrayref, hashref... So, my helper's object code was awful and buggy.

Now, i have two questions:

  1. Can i determine that the method is in the middle of the call chain, rather than the end? Then i could return $self from calc instead of result.
  2. Has it more elegant solution?

Upvotes: 1

Views: 218

Answers (4)

amon
amon

Reputation: 57640

It is not possible for a method to detect whether it is the last call in a chain of calls – and for good reason: $x->a->-b>-c should behave the same as do { my $y = $x->a; my $z = $y->b; $z->c }. I suggest you choose a different API:

  • my $result = A->log($instance->a->b->c);

    Here the logging would be performed by a separate (class-)method. This is of a much cleaner design and trivial to implement:

    sub log :method {
        my ($class, @results) = @_;
        ...;  # print out the results
        return @results;
    }
    

    Do not return a part of the result only – logging should not interfere with the normal data flow. There is one gotcha: The ->c method will necessarily be called in list context, which is why more than one result might be returned. It is not possible to propagate correct context to the ->c method. To do that, we'd have to use a closure:

  • my $result = $instance->log(sub{ $_->a->b->c });

    This could be implemented as

    sub log :method {
        my ($self, $action) = @_;
        local $_ = $self;
        my @results = (wantarray) ? $action->($self) : scalar $action->($self);
        ...;  # perform actual logging
        return (wantarray) ? @results : $results[0];
    }
    

    I think this is the best solution, as it doesn't create any surprising semantics.

  • my $result = $instance->a->b->log->c;

    Here, the log would be invoked before the method of which the result is to be logged. This can be implemented using two strategies:

    The first solution would be to keep an internal flag in your object. This flag would be set by log. When the next method is executed, the flag would be checked before returning. If it's set, the logging would be performed:

    sub log :method {
        my ($self) = @_;
        $self->{_log_next_call} = 1;
        return $self;
    }
    
    sub _do_logging {
        my ($self, @data) = @_;
        $self->{_log_next_call} = 0;
        ...;  # log the data
    }
    
    sub c {
        ...; # do normal stuff
        $self->_do_logging($result) if $self->{_log_next_call};
        return $result;
    }
    

    This is a fairly sane implementation, but requires changes to all methods which may be logged.

    The other implementation strategy would be via a proxy object. The proxy wraps the object performing the actual behavior, but it will log all accesses:

    package Proxy {
        sub new {
            my ($class, $obj) = @_;
            return bless \$obj => $class;
        }
    
        # override "DOES" and "can" for correct proxying
    
        sub DOES {
            my ($self, $role) = @_;
            ...;  # validate that $self is an object
            return $$self->DOES($role);
        }
    
        sub can {
            my ($self, $method) = @_;
            ...;  # validate that $self is an object
            my $code = $$self->can($method);
            return undef unless defined $code;
            return sub {
                my @result = (wantarray) ? $code->(@_) : scalar $code->(@_);
                ...;  # log the result
                return (wantarray) ? @result : $result[0];
            };
        }
    
        # the AUTOLOAD method does the actual proxying,
        # although the interesting stuff is done in "can"
        sub AUTOLOAD {
            my $self = shift;
            my $method = our $AUTOLOAD;
            $method =~ s/\A.*:://s;
            my $code = $self->can($method);
            ...;  # throw error if $code is undef
            return $code->($$self, @_);
        }
    }
    

    Then the log method would just construct the proxy:

    sub log :method {
        my ($self) = @_;
        return Proxy->new($self);
    }
    

Upvotes: 0

tobyink
tobyink

Reputation: 13664

So you want the calc method to return a hashref, except when it's called like:

$object->calc(...)->some_other_method;

... in which case it needs to return $object itself?

My first thought is that this absolutely stinks as an API.

My second thought is that you should be able to accomplish this with Want. But my sense of good taste prevents me from providing a code sample.

Upvotes: 1

Leeft
Leeft

Reputation: 3837

The easiest way to functionally do what you want is probably to add something like a _last_action property to the object, and add an internal method to populate that property. Then each calculation method only needs to call that population method with the data you need which represents the calculation. The ->log method only needs to pull out and process that data, and your calculation methods can stay fairly clean and just return what they need to. I've done something like that below.

That said, your interface isn't the easiest to use, and if your processing gets complicated it adds a lot of overhead to compute results you may not ever use (since you seem to be calculating it all with a very generic named calc). And no, it's impossible to establish how far you are in the call chain.

I'm not entirely sure what you are trying to achieve here, but here's how I would solve what I think you are trying to do ... and note that I use Moose since it (or Moo, or Mouse) makes OO a heck of a lot easier, and operator overloading handles the intricacies. I've also made the class immutable (once set it doesn't change, you get a new object instead) since that's generally a much cleaner interface and easier to maintain.

package MathHelper;
use Moose;
# our basic math operations
use overload fallback => 1,
    '+'   => 'plus',
    '-'   => 'minus',
    '<=>' => 'compare',
    '0+'  => 'to_number',
    '""'  => 'to_number',
;

# allow for a ->new( $value ) call    
around BUILDARGS => sub {
    my $orig = shift;
    my $self = shift;

    if ( @_ == 1 && !ref $_[0] ) {
        return $self->$orig( value => $_[0] );
    } else {
        return $self->$orig( @_ );
    }
};

has value => (
    is => 'ro',
    default => 0,
    documentation => 'Represents the internal value',
);

has _last => (
    is => 'ro',
    default => undef,
    init_arg => 'last',
    documentation => 'The last calculation performed',
);
sub last {
    my $self = shift;
    return $self->_last if defined $self->_last;
    return 'No last result';
}

sub plus {
    my ( $self, $other, $swap ) = @_;
    my $result = $self->value + $other;
    return __PACKAGE__->new(
        value => $result,
        last => "$self + $other = $result",
    );
}

sub minus {
    my ( $self, $other, $swap ) = @_;
    my $result = $self->value - $other;
    $result = -$result if $swap;
    return __PACKAGE__->new(
        value => $result,
        last => ( $swap ) ? "$other - $self = $result" : "$self - $other = $result",
    );
}

sub compare {
    my ( $self, $other, $swap ) = @_;
    if ( $swap ) {
        return $other <=> $self->value;
    } else {
        return $self->value <=> $other;
    }
}

sub to_number {
    my ( $self ) = @_;
    return $self->value;
}

__PACKAGE__->meta->make_immutable;
1;

A little test program:

#!/usr/bin/env perl
use Modern::Perl;
use MathHelper;

my $ten = MathHelper->new( 10 );
my $twenty = MathHelper->new( 20 );
my $thirty = $ten + $twenty;

say "\$ten is $ten";
say "\$twenty is $twenty";
say "\$thirty is $thirty [".$thirty->last."]";

my $tmp = $twenty - $ten;
say "\$ten - \$twenty = $tmp [".$tmp->last."]";

$tmp = $twenty - 3;
say "\$twenty - 3 = $tmp [".$tmp->last."]";

$tmp = $ten - $twenty;
say "\$twenty - \$ten = $tmp [".$tmp->last."]";

$tmp = 3 - $twenty;
say "3 - \$twenty = $tmp [".$tmp->last."]";

say "\$ten is equal to 10" if 10 == $ten;
say "\$ten is smaller than \$twenty" if $ten < $twenty;
say "\$twenty is larger than \$ten" if $twenty > $ten;
say "\$ten + \$twenty is equal to \$thirty" if $ten + $twenty == $thirty;
say "\$ten + \$twenty - 1 is not equal to \$thirty" if $ten + $twenty - 1 != $thirty;

Outputs:

$ten is 10
$twenty is 20
$thirty is 30 [10 + 20 = 30]
$ten - $twenty = 10 [20 - 10 = 10]
$twenty - 3 = 17 [20 - 3 = 17]
$twenty - $ten = -10 [10 - 20 = -10]
3 - $twenty = -17 [3 - 20 = -17]
$ten is equal to 10
$ten is smaller than $twenty
$twenty is larger than $ten
$ten + $twenty is equal to $thirty
$ten + $twenty - 1 is not equal to $thirty

Disclaimer: there may still be mistakes in my code, and there's plenty of room for improvement ... but its a start, and in my experience the only workable solution. I use something very similar in production at $work (that code has multiple internal values though, with months and years).

Upvotes: 0

Colin Phipps
Colin Phipps

Reputation: 908

I do not think that is possible (and if it was, I would not like to use it).

The idiom of chained methods is generally used with methods that mutate the object. So if you want to write it this way, calc() should always return the object and you should have a separate method to return the result. It is then clear what each method is doing.

A->new()->calc(10, 20)->result();
A->new()->calc(10, 20)->log()->result();

Not everyone is a fan of chaining methods anyway. If I were approaching the same problem, I might instead have a verbose property on the object:

A->new(verbose => 1)->calc(10, 20);

and log based on that from within the methods doing the calculation (potentially saving the hassle to commit all the intermediate calculations to private members). But either is valid and may be preferable depending on the calculation.

Upvotes: 2

Related Questions