views:

154

answers:

3

What is the "best" way to use "isa()" reliably? In other words, so it works correctly on any value, not just an object.

By "best", I mean lack of un-handled corner cases as well as lack of potential performance issues, so this is not a subjective question.

This question mentions two approaches that seem reliable (please note that the old style UNIVERSAL::isa() should not be used, with reasons well documented in the answers to that Q):

eval { $x->isa("Class") }
#and check $@ in case $x was not an object, in case $x was not an object

use Scalar::Util 'blessed';
blessed $x && $x ->isa($class);

The first one uses eval, the second uses B:: (at least for non-XS flavor of Scalar::Util).

The first does not seem to work correctly if $x is a scalar containing a class name, as illustrated below, so I'm leaning towards #2 (using blessed) unless somoene indicates a good reason not to.

$ perl5.8 -e '{use IO::Handle;$x="IO::Handle";
  eval {$is = $x->isa("IO::Handle")}; print "$is:$@\n";}'          
1:

Are there any objective reasons to pick one of these two approaches (or a 3rd one i'm not aware of) such as performance, not handling some special case, etc...?

+8  A: 

The Scalar::Util implementation is categorically better. It avoids the overhead of the eval {} which always results in the setting of an additional variable.

perl -we'$@=q[foo]; eval {}; print $@'

The Scalar::Util implementation is easier to read (it doesn't die for a reason that is unknown to the code). If the eval fails too, I believe what happens is you have walk backwards in the tree to the state prior to the eval -- this is how resetting state is achieved. This comes with additional overhead on failure.

Benchmarks

Not an object at all

          Rate eval   su
eval  256410/s   -- -88%
su   2222222/s 767%   --

Object passing isa check

          Rate   su eval
su   1030928/s   -- -16%
eval 1234568/s  20%   --

Object failing isa check

         Rate   su eval
su   826446/s   --  -9%
eval 909091/s  10%   --

Test code:

use strict;
use warnings;
use Benchmark;
use Scalar::Util;

package Foo;

Benchmark::cmpthese(
    1_000_000
    , {
        eval => sub{ eval{ $a->isa(__PACKAGE__) } }
        , su => sub { Scalar::Util::blessed $a && $a->isa(__PACKAGE__) }
    }
);

package Bar;

$a = bless {};

Benchmark::cmpthese(
    1_000_000
    , {
        eval => sub{ eval{ $a->isa(__PACKAGE__)} }
        , su => sub { Scalar::Util::blessed $a && $a->isa(__PACKAGE__) }
    }
);

package Baz;

$a = bless {};

Benchmark::cmpthese(
    1_000_000
    , {
        eval => sub{ eval{ $a->isa('duck')} }
        , su => sub { Scalar::Util::blessed $a && $a->isa( 'duck' ) }
    }
);

I used This is perl, v5.10.1 (*) built for i486-linux-gnu-thread-multi, and Scalar::Util, 1.21

Evan Carroll
@Evan - thanks! I am not familiar enough with internal workings of eval so this basically addresses my vague performance concerns with specific details.
DVK
@DVK, just updated with benchmarks.
Evan Carroll
+3  A: 

This might sound a little bit harsh to Perl, but neither one of these is ideal. Both cover up the fact that objects are a tack on to Perl. The blessed idiom is wordy and contains more than a couple simple pieces.

blessed( $object ) && object->isa( 'Class' )

I would prefer something more like this:

object_isa( $object, 'Class' )

There is no logical operation to get wrong, and most of the unfit uses will be weeded out by the compiler. (Quotes not closed, no comma, parens not closed, calling object_ia instead...)

It would take undefined scalars, simple scalars (unless they are a classname that is a Class), unblessed references, and blessed references that do not extend 'Class' and tell you that no, they are not Class objects. Unless we want to go the route of autobox-ing everything, we're going to need a function that tells us simply.

Perhaps there might be a third parameter for $how_close, but there could also be something like this:

if ( my $ranking = object_isa( $object, 'Class' )) { 
   ...
}
else { 
   given ( $ranking ) { 
       when ( NOT_TYPE )    { ... }
       when ( NOT_BLESSED ) { ... }
       when ( NOT_REF )     { ... }
       when ( NOT_DEFINED ) { ... }
   }
}

About the only way I can see that we could return this many unique falses is if $ranking was blessed into a class that overloaded the boolean operator to return false unless the function returned the one value indicating an ISA relationship.

However, it could have a few members: EXACTLY, INHERITS, 'IMPLEMENTS,AGGREGATESor evenMOCKS`

I get tired of typing this too:

$object->can( 'DOES' ) && $object->DOES( 'role' )

because I try to implement the future-facing DOES in lesser perls (on the idea that people might frown on my polluting UNIVERSAL) on them.

Axeman
All of these points are near moot with `Moose` which provides a typing system that at the very least will keep non-objects out for you. (though all of these points are valid under the hood).
Evan Carroll
@Axeman => your answer inspired mine
Eric Strom
+3  A: 

You can wrap the safety checks in a scalar and then use the scalar as a method to keep things clean:

use Scalar::Util 'blessed';

my $isa = sub {blessed $_[0] and $_[0]->isa($_[1])};

my $obj;

if ($obj->$isa('object')) { ... } # returns false instead of throwing an error 

$obj = {};

if ($obj->$isa('object')) { ... } # returns false as well

bless $obj => 'object';

if ($obj->$isa('object')) { say "we got an object" }

Note that $obj->$isa(...) is just a different spelling of $isa->($obj, ...) so no method call actually takes place (which is why it avoids throwing any errors).

And here is some code that will allow you to call isa on anything and then inspect the result (inspired by Axeman's answer):

{package ISA::Helper;
    use Scalar::Util;
    sub new {
        my ($class, $obj, $type) = @_;
        my $blessed = Scalar::Util::blessed $obj;
        bless {
            type    => $type,
            obj     => $obj,
            blessed => $blessed,
            isa     => $blessed && $obj->isa($type)
        } => $class
    }
    sub blessed         {$_[0]{blessed}}
    sub type            {$_[0]{isa}}
    sub ref     {ref     $_[0]{obj}}
    sub defined {defined $_[0]{obj}}

    use overload fallback => 1,
                 bool     => sub {$_[0]{isa}};
    sub explain {
        my $self = shift;
        $self->type    ? "object is a $$self{type}" :
        $self->blessed ? "object is a $$self{blessed} not a $$self{type}" :
        $self->ref     ? "object is a reference, but is not blessed" :
        $self->defined ? "object is defined, but not a reference"
                       : "object is not defined"
    }
}
my $isa = sub {ISA::Helper->new(@_)};

By placing the code reference in a scalar, it can be called on anything without error:

my @items = (
    undef,
    5,
    'five',
    \'ref',
    bless( {} => 'Other::Pkg'),
    bless( {} => 'My::Obj'),
);

for (@items) {
    if (my $ok = $_->$isa('My::Obj')) {
        print 'ok: ', $ok->explain, "\n";
    } else {
        print 'error: ', $ok->explain, "\n";
    }
}

print undef->$isa('anything?')->explain, "\n";

my $obj = bless {} => 'Obj';
print $obj->$isa('Obj'), "\n";

my $ref = {};
if (my $reason = $ref->$isa('Object')) {
    say "all is well"
} else {
    given ($reason) {
        when (not $_->defined) {say "not defined"}
        when (not $_->ref)     {say "not a reference"}
        when (not $_->blessed) {say "not a blessed reference"}
        when (not $_->type)    {say "not correct type"}
    }
}

this prints:

error: object is not defined
error: object is defined, but not a reference
error: object is defined, but not a reference
error: object is a reference, but is not blessed
error: object is a Other::Pkg not a My::Obj
ok: object is a My::Obj
object is not defined
1
not a blessed reference

If anyone thinks this is actually useful, let me know, and I will put it up on CPAN.

Eric Strom