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

Tagged in : roles, advent