Redux redux (via Pollux)

« Return to Our Notebook

Redux redux (via Pollux)

Redux is a small JavaScript library that is quite popular at the moment. Liberally inspired by functional programming principles, it offers a state container that is accessed and modified via message passing.

Thanks to this message passing, and a strong emphasis on immutability and pure functions, it minimizes surprises and maximizes sanity. One of its beautiful promises, for example, is that since the state is only modified via messages (or actions) and pure functions, one can consistently replay the actions of an application and end up in exactly the same final state.

As I was reading and playing with Redux, I began to wonder... This is a blissfully small library. How easy would it be to port it to Perl? In the name of science, I had to try.

So what we'll do in this blog entry is go through most of the Redux tutorial and implement the different notions and examples as we go along. Since what you are about to see is still only the result of a few hours of playful hacking, it will be in no way a one-to-one parity with the real deal. And yet, I think you'll be pleasantly surprised...

Actions

Our first stop is an easy one. The messages that Redux uses, called actions, are just JavaScript objects, aka good old hashes, with a type key. So, really, we could just do

my $action = {
    type => 'ADD_TODO',
    text => 'Build my first Redux app',
};

and call it a day.

But, honestly, where is the fun in that? Instead, let's create a class that will help generate all those different action types and action instances.

This is something we could do in many ways, but let's try to stay minimalistic. We just want a quick way to declare those new types with whatever fields they might have. Something like:

my $AddTodo  = Pollux::Action->new( 'ADD_TODO', 'text' );
my $DoneTodo = Pollux::Action->new( 'DONE_TODO', 'index' );

Of course, we want to be able to create actions with those types.

my $todo = $AddTodo->( 'do the thing' );
# $todo is now { type => 'ADD_TODO', text => 'do the think' }

There will be a lot of comparisons in our future, so we might want to sprinkle a little bit of overloading sugar on top of it as well. As seen in the previous snippet, using the type as a coderef creates an action. We will also want a quick way to get the type name.

print "$AddTodo"; # will print 'ADD_TODO'

And a way to compare actions against types.

print "match" if $todo ~~ $AddTodo;

And since "Immutability" is the word of the day, let's make sure once an action is created, we can't go ahead and mess with it either.

And here is how we shall do all this.

package Pollux::Action;

use List::MoreUtils qw/ zip /;
use Const::Fast;

use Moo;
use MooseX::MungeHas 'is_ro';

use experimental 'postderef';

use overload
    '""'  => sub { $_[0]->type },
    '&{}' => sub {
        my $self = shift;
        return sub {
            const my $r, {
                type => $self->type,
                $self->has_fields ? zip $self->fields->@*, @_ : ()
            };
            $r;
        }
    },
    '~~' => sub {
        my( $self, $other ) = @_;
        return $self->type eq ( ref $other ? $other->{type} : $other );
    },
    fallback => 1;

has type   => ( required  => 1 );

has fields => ( predicate => 1 );

sub BUILDARGS {
    my $class = shift;

    my %args = ( type => uc shift );

    $args{fields} = [ @_ ] if @_;

    return \%args;
}

1;

Three notes on that class:

  • Pollux? Yes, Pollux. Mostly because naming things is hard, and I could not come up with a single clever pun involving Redux and Perl. So I repeated both terms over and over again until they mushed together as Pollux. Could have been worse.

  • To ensure the generated hashes are not modified, I lock them up with Const::Fast.

  • I totally put dibs on $foo ? @_ : () and henceforth dub it "the fat-lipped Elvis operator".

A quick sidetrip to helper land

We already saw that we'll be using Const::Fast to make sure our data structures aren't tampered with. On the flip side, if we do that, we should also make it easy to clone and merge structures so that the immutability won't be too much of a pain in the tuckus. For that, we'll use the ever reliant Clone and Hash::Merge.

package Pollux;

use strict;
use warnings;

use Hash::Merge    qw/ merge/;
use Clone          qw/ clone /;
use List::AllUtils qw/ pairmap reduce /;

use experimental 'signatures';

use parent 'Exporter';

our @EXPORT = qw/ clone merge combine_reducers /;

Hash::Merge::specify_behavior({
        SCALAR => {
            map { $_ => sub { $_[1] } } qw/ SCALAR ARRAY HASH /
        },
        ARRAY => {
            map { $_ => sub { $_[1] } } qw/ SCALAR ARRAY HASH /
        },
        HASH => {
            map { $_ => sub { $_[1] } } qw/ SCALAR ARRAY /,
            HASH   => sub { Hash::Merge::_merge_hashes( $_[0], $_[1] ) },
        },
}, 'Pollux');

1;

Reducers

Reducers are also pretty easy. They are, after all, functions that take a state and an action, and spit back the new state.

There are two bits of JavaScript magic we won't be able to fully reproduce, though. The first is the reducer signature

function myReducer (state=defaultState,action) { ... }

which doesn't work in Perl as the take-a-default-value argument must be on the tail end of the argument list. So I'll just decree that the order of arguments is flipped around in Pollux

sub my_reducer ( $action, $state='MY DEFAULT' ) { ... }

So there.

Second thing is the nifty expension that ES6 can do on objects:

{
    reducerOne,
    reducerTwo
}

// equivalent to
{
    reducerOne: reducerOne,
    reducerTwo: reducerTwo
}

// where the values are the functions

I began to tinker with array refs of reducer names and peeks into the caller namepaces for the function names but, y'know what?, No, it's not worth it. Again, for Pollux I'll be lazy and say that

my $reducer = combine_reducers(
    reducer_one => \&reducer_one,
    reducer_two => \&reducer_two,
);

will be good enough.

Knowing that, all we really need is that combine_reducers function, which we'll add to the main Pollux class:

sub combine_reducers {
    my @reducers = @_;

    return sub ( $action=undef, $store={} ) {
        reduce { merge( $a, $b ) }
            $store,
            pairmap {
                +{ $a => $b->($action, exists $store->{$a} ? $store->{$a} : () ) }
            } @reducers;
        }
}

And that's pretty much all we need to create and mix our reducers.

use 5.20.0;

use warnings;

use Test::More;

use experimental 'switch', 'signatures';

use Pollux;
use Pollux::Action;

my $AddTodo
    = Pollux::Action->new( 'ADD_TODO', 'text' );
my $CompleteTodo
    = Pollux::Action->new( 'COMPLETE_TODO', 'index' );
my $SetVisibilityFilter
    = Pollux::Action->new( 'SET_VISIBILITY_FILTER', 'filter' );

sub visibility_filter( $action, $state = 'SHOW_ALL' ) {
    given ( $action ) {
        return $action->{filter} when $SetVisibilityFilter;
        default{ return $state }
    }
}

sub todos( $action=undef, $state=[] ) {
    given( $action ) {
        when( $AddTodo ) {
            return [
                @$state,
                { text => $action->{text}, completed => 0 }
            ];
        }
        when ( $CompleteTodo ) {
            my $s = clone($state);
            $s->[ $action->{index} ]{completed} = 1;
            return $s;
        }
        default{ return $state }
    }
}

my $todo_app = combine_reducers(
    visibility_filter => \&visibility_filter,
    todos             => \&todos,
);

my $state = $todo_app->();

is_deeply $state => {
    visibility_filter => 'SHOW_ALL',
    todos => [],
}, "initial";

$state = $todo_app->( $SetVisibilityFilter->('HIDE_ALL'), $state );

is_deeply $state => {
    visibility_filter => 'HIDE_ALL',
    todos => [],
}, "SET_VISIBILITY_FILTER";

$state = $todo_app->( $AddTodo->('do stuff'), $state );

is_deeply $state => {
    visibility_filter => 'HIDE_ALL',
    todos => [ { text => 'do stuff', completed => 0 } ],
}, "add todo";

$state = $todo_app->( $CompleteTodo->(0), $state );

is_deeply $state => {
    visibility_filter => 'HIDE_ALL',
    todos => [ { text => 'do stuff', completed => 1 } ],
}, "complete todo";

The Store

Time to put things together with the store.

The API, happily, is a simple one. Before all, it needs to be able to keep a state. And because we can, let's make sure it's always an immutable structure via Type::Tiny coercion.

package Pollux::Store;

use Moose;
use MooseX::MungeHas 'is_ro';

use experimental  qw/ signatures /;

use Type::Tiny;
use Types::Standard qw/ CodeRef ArrayRef HashRef Any /;
use Const::Fast;

has state => (
    is => 'rwp',
    predicate => 1,
    coerce    =>  1,
    isa => Type::Tiny->new->plus_coercions(
        Any ,=> sub { const my $immu = $_; return $immu }
    ),
);

To modify that state, the store also needs a reducer.

use Pollux;

has reducer => (
    required => 1,
    coerce   => 1,
    isa      => Type::Tiny->new(
                    parent => CodeRef,
                )->plus_coercions(
                    HashRef ,=> sub { combine_reducers( %$_ ) }
                ),
);

Next, to feed the reducer, the dispatch method.

sub dispatch($self,$action) {
    $self->_set_state( $self->reducer->(
        $action, $self->has_state ? $self->state : ()
    ));
}

And, finally, let's not forget the ability to have callback subscribers to state changes.

use Scalar::Util qw/ refaddr /;

use experimental 'current_sub';

has '+state' => (
    trigger   => sub($self,$new,$old=undef) {
        no warnings 'uninitialized';

        return if $new eq $old;

        $self->unprocessed_subscribers([ $self->all_subscribers ]);

        $self->notify;
    },
);

has subscribers => (
    traits => [ 'Array' ],
    is => 'rw',
    default => sub { [] },
    handles => {
        all_subscribers  => 'elements',
        add_subscriber   => 'push',
        grep_subscribers => 'grep',
    },
);

has unprocessed_subscribers => (
    traits     => [ 'Array' ],
    is         => 'rw',
    default    => sub { [] },
    handles    => {
        shift_unprocessed_subscribers => 'shift',
    },
);

sub subscribe($self,$code) {
    $self->add_subscriber($code);

    my $addr = refaddr $code;

    return sub { $self->subscribers([
        $self->grep_subscribers(sub{ $addr != refaddr $_ })
    ]) }
}

sub notify($self) {
    my $sub = $self->shift_unprocessed_subscribers or return;
    $sub->($self);
    goto __SUB__;  # tail recursion!
}

And that's it. We now have a functional redux-like store!

use strict;
use warnings;

use Test::More tests => 2;

use experimental 'switch', 'signatures';

use Pollux;
use Pollux::Action;
use Pollux::Store;

my $AddTodo
    = Pollux::Action->new( 'ADD_TODO', 'text' );
my $CompleteTodo
    = Pollux::Action->new( 'COMPLETE_TODO', 'index' );
my $SetVisibilityFilter
    = Pollux::Action->new( 'SET_VISIBILITY_FILTER', 'filter' );

sub visibility_filter($action, $state = 'SHOW_ALL' ) {
    given ( $action ) {

        return $action->{filter} when $SetVisibilityFilter;

        default{ return $state }

    }
}

sub todos($action=undef,$state=[]) {
    given( $action ) {
        when( $AddTodo ) {
            return [ @$state, { text => $action->{text}, completed => 0 } ];
        }
        when ( $CompleteTodo ) {
            my $i = 0;
            [ map { ( $i++ != $action->{index} ) ? $_ : merge( $_, { completed => 1 } ) } @$state ];
        }
        default{ return $state }
    }
}

my $store = Pollux::Store->new( reducer => {
    visibility_filter => \&visibility_filter,
    todos             => \&todos
});


my @log;

push @log, $store->state;

my $unsubscribe = $store->subscribe(sub($store) {
    push @log, $store->state;
});

$store->dispatch($AddTodo->('Learn about actions'));
$store->dispatch($AddTodo->('Learn about reducers'));
$store->dispatch($AddTodo->('Learn about store'));
$store->dispatch($CompleteTodo->(0));
$store->dispatch($CompleteTodo->(1));
$store->dispatch($SetVisibilityFilter->('SHOW_COMPLETED'));

$unsubscribe->();

$store->dispatch($AddTodo->('One more'));

is scalar @log => 7, '6 events + initial state';

is_deeply $store->state, {
    'todos' => [
        {
            completed => 1,
            text      => 'Learn about actions'
        },
        {
            completed => 1,
            text      => 'Learn about reducers'
        },
        {
            completed => 0,
            text      => 'Learn about store'
        },
        {
            completed => 0,
            text      => 'One more'
        }
    ],
    visibility_filter => 'SHOW_COMPLETED'
}, 'final state';

Middleware

Last major piece still missing: middlewares. To add this functionality, we'll alter the dispatch method of the store just a wee bit.

use List::Util qw/ reduce /;

has middlewares => (
    is => 'ro',
    traits => [ qw/ Array / ],
    default => sub { [] },
    handles => {
        all_middlewares => 'elements',
    },
);

sub _dispatch_list($self) {
    return $self->all_middlewares, sub { $self->_dispatch(shift) };
}

sub dispatch($self,$action) {
    ( reduce {
        # inner scope to thwart reduce scoping issue
        {
            my ( $inner, $outer ) = ($a,$b);
            sub { $outer->( $self, $inner, shift ) };
        }
    } reverse $self->_dispatch_list )->($action);
}

sub _dispatch($self,$action) {
    $self->_set_state( $self->reducer->(
        $action, $self->has_state ? $self->state : ()
    ));
}

Yup. When I said 'a wee bit', I really meant a wee bit. You'll notice that, since in Perl-space we don't have the nifty (x)=>(y)=>(z) currying that JavaScript allows, we went for a more boring sub($store,$next,$action) signature. But beside that, we're good to go.

use strict;
use warnings;

use Test::More tests => 2;

use Pollux;
use Pollux::Store;
use Pollux::Action;

use experimental 'signatures';

my $Action = Pollux::Action->new( 'ACTION', 'text' );

my @middlewares =  map {
    my $x = $_;
    sub($store,$next,$action) {
        $next->( $Action->( $action->{text} . $x ) )
    }
} 1..3;

my $store = Pollux::Store->new(
    middlewares => \@middlewares,
    reducer     => sub($action,$state='') {
        $state . $action->{text}
    },
);

$store->dispatch( $Action->( 'foo' ) );

is $store->state => 'foo123', "middleware run in order";

subtest "middleware doing a dispatch" => sub {
    my $Action = Pollux::Action->new( 'ACTION', 'text' );

    my $store = Pollux::Store->new(
        middlewares => [
            sub ($store,$next,$action) {
                $next->( $Action->( $action->{text} . 'o' ) )
            },
            sub ($store,$next,$action) {
                $store->dispatch( $Action->('bar') )
                    if $action->{text} eq 'foo'; $next->($action)
            },
        ],
        reducer => sub($action,$state='') {
            $state . $action->{text}
        },
    );

    $store->dispatch( $Action->( 'fo' ) );

    is $store->state => 'barofoo', "middleware can dispatch";
};

Tadah

And here we are, with a basic but functional Perl Redux proof of concept. The jury is still out as to whether it'll make its way to CPAN. But even if it doesn't, I hope it still helped to show that Perl truly has a little bit of everything for everyone, including the functional programming crowd.

We solve problems with technology. What can we solve for you?

Reach Out

t: 800.646.0188