People arriving from other programming languages or from using Moose might be expecting to find a system for types checking, but Moo being the Minimal Object Orientation system of Perl, does not provide any type-checking.

However you can easily add type checking of your own. Let's see how.

Type restrictions with regex

examples/mootype/Person.pm

package Person;
use Moo;

has name => (is => 'rw');
has age  => (
    is  => 'rw',
    isa => sub {
       die "'$_[0]' is not an integer!"
          if $_[0] !~ /^\d+$/;
    },
);

1;

In the above code, the Person class has two attributes. name has no restrictions, but age does have one. It is declared using the isa keyword of Moo. The value of the isa key is a reference to an anonymous function. This function is called every time when someone tries to assign a value to this attribute via the constructor or the setter of the attribute.

The value to be assigned to the attribute is passed to the function as its first parameter. In our example we used the rather unpleasant looking $_[0] to access this value. (We could have shift-ed it to an internal variable, but in such a short subroutine, this seems to be the more readable version. We can use then any code to check if the value matches the expectations and throw an exception if it does not. In this case we checked if the value consist of one or more digits: /^\d+$/ and we threw a very simple, text-based exception using die.

Let's see how does that work when we use the module?

examples/mootype/student_1.pl

use strict;
use warnings;
use 5.010;

use Person;
my $student = Person->new( name => 'Foo', age => 22 );
say $student->name;
say $student->age;
$student->age('young');
say $student->age;

The output of this code will look like this:

Foo
22
isa check for "age" failed: 'young' is not an integer! at Person.pm line 8.

Calling the constructor worked. Calling the name and the age methods worked and the say function printed the values. Then calling $student->age('young'); threw an exception. The exception contains some text Moo provided and the text we included in the die call. The last call to say was never reached.

Let's see what happens when the incorrect value is already provided in the constructor:

examples/mootype/student_2.pl

use strict;
use warnings;
use 5.010;

use Person;
my $student = Person->new( name => 'Foo', age => 'old' );
say $student->name;

The exception is thrown right during the call of the constructor:

isa check for "age" failed: 'old' is not an integer! at Person.pm line 8.

Where is the problem?

Unfortunately in both cases, the error location was given in the Person.pm file. Indeed that's the place where the exception was thrown, but that's not very useful for the developer who just happens to use the Person module. After all, the real source of the problem is in the script that called the constructor or the setter with incorrect value and not in the Person.pm module.

Veteran Perl developers might immediately remember that the croak function of the Carp module is useful in such situations. It is almost the same as die except when it reports the location of the error it will report the place where the current function was called.

So we include:

use Carp qw(croak);

and replace the die call by croak. The result:

isa check for "age" failed: 'old' is not an integer! at (eval 11) line 37.

Not exactly what we expected. Apparently Moo wraps the isa-checking code in a string-eval of its own.

Let's try the confess function of Carp then. That too is similar to die but it provides the full stack-trace from point where it was called up to your main script.

We put this in the code:

use Carp qw(confess);

And replaced the call to croak by a call to confess. The result:

isa check for "age" failed: 'old' is not an integer! at Person.pm line 11.
    Person::__ANON__('old') called at (eval 11) line 37
    Person::new('Person', 'name', 'Foo', 'age', 'old') called at t.pl line 6

Here we can see that the problem was noticed in line 11 of the Person.pm file, but it was called in line 6 of the t.pl file (my example script).

This is probably a better way to throw the exception than either of the above 2.

Just for the record let's see the Person.pm file now:

package Person;
use Moo;

use Carp qw(confess);

has name => (is => 'rw');
has age  => (
    is  => 'rw',
    isa => sub {
       confess "'$_[0]' is not an integer!"
          if $_[0] !~ /^\d+$/;
    },
);

1;

Other ways to check if value is a number

In the previous example we used a regular expression to check if the assigned value contains one or more digits. There are other cases when you'd like to check if the value "is a number"? Perl does not have an is_number function, but there is a function called looks_like_number in the Scalar::Util module. It basically checks if the given value can be automatically converted to a number without any warning. So we can write:

use Scalar::Util qw(looks_like_number);

to load the module and import the function, and rewrite the isa-checking:

    isa => sub {
       confess "'$_[0]' is not a number!"
          unless looks_like_number $_[0];
    },

Pre-defined types

While Moo itself does not have types defined, there are several extension that provide type definitions. For example MooX::Types::CLike provides types a C programmer might be familiar with and MooX::Types::MooseLike imitates the types of MooseX::Types.

It can be used to create your own type definitions, but it already comes with several types listed in MooX::Types::MooseLike::Base.

You can use the types it provides without any extra work.

Just load the types using use MooX::Types::MooseLike::Base qw(:all); and then you can use the types as the values of the isa field. For example we used Int in the following example:

package Person;
use Moo;

use MooX::Types::MooseLike::Base qw(:all);

has name => (is => 'rw');
has age  => (
    is  => 'rw',
    isa => Int,
);

1;

The resulting error look like this:

isa check for "age" failed: old is not an integer! at (eval 13) line 37.
   Person::new('Person', 'name', 'Foo', 'age', 'old') called at t.pl line 6

Apparently this module already provides a stack trace but excludes the line where the exception was thrown.

That's the basics of adding type checking to

A little warning

Beware though. As the actual objects created by Moo are just blessed references to hashes, any user will have direct access to the attributes. Moo cannot enforce the type restrictions if someone directly accesses the internals of the object. Of course no one will do that, right? See the bad example:

use strict;
use warnings;
use 5.010;

use Person;
my $student = Person->new( name => 'Foo', age => '20' );
say $student->age;
$student->{age} = 'young';     # BAD BAD BAD!
say $student->age;

The output then:

20
young

This is basically a limitation of Perl. There is a solution to this using, so called inside-out objects, but they have a complexity price and they usually not worth the hassle.

Perl::Critic

A better approach is to use the regular objects and use Perl::Critic to locate code that breaks the encapsulation. Install Perl::Critic::Nits and check your code for violation of Perl::Critic::Policy::ValuesAndExpressions::ProhibitAccessOfPrivateData

If you have them in your code you will get reports like this:

Private Member Data shouldn't be accessed directly at line 8, column 1.
Accessing an objects data directly breaks encapsulation and should be avoided.  Example: $object->{ some_key }.  (Severity: 5)

You can then use the strategy to improve your Perl code one policy at a time.

Comments

Could you please remove any references to `Moose` and `MooX::Types::MooseLike::Base`, just use `Types::Standard`, part of the `Type::Tiny` suite, which is more universal etc.


Hi Gabor, I greatly appreciate these articles. Thank you for making these available. For the last section of using perlcritic on this page, is it possible to add how we use perlcritic? I did install Perl::Critic and Perl::Critic::Nits and was trying to use "perl -c code.pl" and wondering why I don't see the warning :-) Then I visited "improve your Perl code" link and realized what I was doing wrong.