Day 13: Taking on Roles
In my previous Advent article, I created higher-order promises and showed you how to use them. I didn't show you the magic of how they work. Now I'll develop another example but from the other direction.
There are times that I want Mojo::File to act a bit differently than it does. Often I have a path where I want to combine only the basename with a different directory. I end up making Mojo::File
objects for both and then working with the directory object to get what I want:
use Mojo::File qw(path);
my $path = Mojo::File->new( '/Users/brian/bin/interesting.txt' );
my $dir = Mojo::File->new( '/usr/local/bin' );
my $new_path = $dir->child( $path->basename );
say $new_path; # /usr/local/bin/interesting.txt
That's annoying. I don't like that it takes so many steps. There are a few methods that I'd like instead. I'd rather be able to write it like this, where I start with the interesting file and keep working on it instead of switching to some other object:
use Mojo::File qw(path);
my $new_path = Mojo::File
->new( '/Users/brian/bin/interesting.txt' )
->rebase( '/usr/local/bin' ); # this isn't a method
say $new_path; # /usr/local/bin/interesting.txt
I could go through various Perl tricks to add this method to Mojo::File
through monkey patching or subclassing. But, as usual, Mojolicious anticipates my desire and provides a way to do this. I can add a role,
You can read about roles on your own while I jump into it. First, I create a class to represent my role. I define the method(s) I want. I use the name of the package I want to affect, add ::Role::
, then the name I'd like to use; it's not important that its lowercase. Mojo::Base
sets up everything I need when I import -role
:
package Mojo::File::Role::rebase {
use Mojo::Base qw(-role -signatures);
sub rebase ($file, $dir) {
$file->new( $dir, $file->basename )
}
}
I apply my new functionality by using with_roles
on the class I want to affect. Since I used the naming convention by prefixing it with the target class (Mojo::File
), then ::Role::
, then the short name I want. When I apply this, I can leave off most of the package name and use the short name preceded by a plus sign:
my $file_class = Mojo::File->with_roles( '+rebase' );
Alternately I could have typed out the full package name:
my $file_class = Mojo::File->with_roles(
'Mojo::File::Role::rebase' );
I'd need to use this if I didn't follow the naming convention:
my $file_class = Mojo::File->with_roles(
'I::Totally::Rejected::The::Convention::rebase' );
The $file_class
is a string with the new class name. Behind that class there is some multiple inheritance magic that you'll be much happier ignoring. I don't need to use a bareword class name to call class methods; a string in a scalar variable works just as well. Now I can use my rebase
:
say $file_class
->new( '/Users/brian/bin/interesting.txt' )
->rebase( '/usr/local/bin/' );
That's much cleaner than what I was doing before and I like how this flows. But what if I get an already-created Mojo::File object from something else? I can apply the role ad hoc too:
my $file = path( '/Users/brian/bin/interesting.txt' );
say $file
->with_roles( '+rebase' )
->rebase( '/usr/local/bin/' );
I can go further. Any methods I add to my role become part of the class. I often want to get the digests of files and although Mojo::Util makes that easier with some convenience functions, I want even more convenience. I add a couple of methods to my role to do the slurping for me:
use Mojo::File;
package Mojo::File::Role::MyUtils {
use Mojo::Base qw(-role -signatures);
use Mojo::Util qw(md5_sum sha1_sum);
sub rebase ($file, $dir) {
$file->new( $dir, $file->basename )
}
sub md5 ($file) {
md5_sum( $file->slurp )
}
sub sha1 ($file) {
sha1_sum( $file->slurp )
}
}
my $file = Mojo::File
->with_roles( '+MyUtils' )
->new(shift);
say $file->sha1;
say $file->md5;
You can read more about roles in Joel Berger's 2017 Mojolicious Advent Calendar entry Day 13: More About Roles. Curiously that was on Day 13 too, although I don't think Joel or I were clever enough to plan that.
Image by Viv Lynch CC BY-NC-ND 2.0
brian d foy
brian d foy is the author of Mastering Perl and Learning Perl 6, and the co-author of Learning Perl, Intermediate Perl, Programming Perl, and Effective Perl Programming.