Use the infix class instance operator

Perl v5.32 adds Paul Evans’s infix isa operator—the “class instance operator” as an experimental feature. It still has some issues to work out which prevent its use at the moment, but it looks promising. It subverts how the UNIVERSAL::isa does its job and breaks that in the process. As an experimental feature, that’s fine, but you shouldn’t use this until that’s worked out.

There’s no word on versions for can or does.

One of the delightful things to note about this is addition is that it is one of the features whose development took place almost entirely through a GitHub issue and pull request. GitHub is now the primary repository for the Perl code, and has been since October 2019. This is a feature that I’ll want to use right away in new production code.

The UNIVERSAL class, from which all other classes ultimately inherit, provides the basic isa method. Pronouce that IS-A, like “a cat is a mammal”. I think the best explanation is in the now-removed Perl Barnyard OO Tutorial (perlboot). It’s also how we explain object-orientation in Intermediate Perl. We showed isa in Effective Perl Programming too.

One way to use it involves eval so that a non-object can still return false instead of a method error. If it’s not an object it doesn’t inherit:

if( eval { $var->isa( $some_package ) } ) {
	... do stuff ...
	}

Sometimes you see this in function form to avoid the error if $var is not an object, but this subverts any overridden isa methods. Sadly, I think I use this somewhere in Effective Perl Programming too:

if( UNIVERSAL::isa( $var, $some_package ) ) {
	... do stuff ...
	}

This new infix feature removes some of the syntax and obviates the eval by returning false if $var is not an object:

use v5.31.7;
use warnings;
use feature qw(isa);
no warnings qw(experimental::isa);

if( $var isa $some_package ) {
	... do stuff ...
	}

The lefthand side can be anything. If it’s not an object, it’s false. If it’s an object that inherits from the class name on the righthand side, it’s true:

use v5.31.7;
use warnings;
use feature qw(isa);
no warnings qw(experimental::isa);

package Parent { sub new { bless {}, $_[0] } }
package Child  { our @ISA = qw(Parent) }

my $obj = Child->new;

my @tests = (
	[ 'string',   'Some::Class',  'Plain string'            ],
	[ 0xDEADBEEF, "Some::Class",  'Plain number'            ],
	[ undef,      "Some::Class",  'undef'                   ],
	[ $obj,       "Parent",       'obj of Parent'           ],
	[ $obj,       "Child",        'obj of Child'            ],
	[ $obj,       "UNIVERSAL",    'obj of UNIVERSAL'        ],
	[ $obj,       "Other::Class", 'not obj of Other::Class' ],
	[ [],         "ARRAY",        'array ref'               ],
	[ {},         "HASH",         'hash ref'                ],
	);

foreach my $test ( @tests ) {
	my( $candidate, $class, $label ) = $test->@*;

	say $label if $candidate isa $class;
	}

say "Loaded class, bareword, Parent" if $obj isa Parent;
say "Loaded class, bareword, Child"  if $obj isa Child;

# Normally, a bareword class needs to be loaded. This ignores
# that.
say "Non-loaded class, bareword" if $obj isa Other::Class;

say "Done!";

This is the entire output, warnings (none!) and all. The program goes all the way to the end without die-ing, even though I use a class name that I haven’t defined:

obj of Parent
obj of Child
obj of UNIVERSAL
Loaded class, bareword, Parent
Loaded class, bareword, Child
Done!

You can see more examples in the test file, t/op/isa.t.

The Problem

Note: this is fixed in v5.34, but still broken in v5.32.1..

If you use the new infix isa, it breaks the normal use of UNIVERSAL::isa, even outside the scope that enabled it:

use v5.32;
use warnings;

package Thingy { }

my $x = bless {}, 'Thingy';

eval {
	use experimental 'isa';
	say "Inside infix: ", $x isa 'Thingy';
	say "Inside ->isa: ", $x->isa('Thingy');
	};
say "AT 1: $@";

say "Outside ->isa: ", eval { $x->isa('Thingy') };
say "AT 2: $@";

The output shows that the method call inside the same scope as the feature fails. It also shows that the method call outside the scope fails.

Inside infix: 1
AT 1: Can't locate object method "isa" via package "Thingy" at /Users/brian/Desktop/isa.pl line 11.

Outside ->isa:
AT 2: Can't locate object method "isa" via package "Thingy" at /Users/brian/Desktop/isa.pl line 15.

This appears to happen only within the same file (compilation unit), so a different module that uses it is safe. If you don’t mix both uses in the same file, you might be okay. However, anyone who works on that file needs to know not to mix them, which means they need to know which one you’ve chosen to use. That’s too high of a maintenance burden in most cases.

Things to Remember

  • UNIVERSAL::isa subverts overridden isa methods.
  • Calling the isa blows up if the invocant is not an object.
  • The experimental infix isa solves these problems.
  • You can’t mix both in the same compilation unit.