package Acme::Partitioner;
use 5.012000;
use strict;
use warnings;
require Exporter;
our @ISA = qw(Exporter);
our %EXPORT_TAGS = ( 'all' => [ qw(
) ] );
our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
our @EXPORT = qw(
);
our $VERSION = '0.01';
sub using {
my ($class, @list) = @_;
bless {
by_string => { map { $_ => 0 } @list },
sublists => [\@list],
}, $class;
}
sub once_by {
my ($self, $sub) = @_;
Acme::Partitioner::Actor::_new(undef, "once", $sub, $self);
}
sub partition_of {
my ($self, $item) = @_;
$self->{by_string}{$item};
}
sub items_in {
my ($self, $partition) = @_;
@{ $self->{sublists}[$partition] }
}
sub size {
my ($self) = @_;
scalar @{ $self->{sublists} }
}
sub all_partitions {
my ($self) = @_;
map { [@$_] } @{ $self->{sublists} }
}
package Acme::Partitioner::Actor;
use 5.012000;
use strict;
use warnings;
sub _new {
my ($old, $type, $sub, $partitioner) = @_;
$partitioner //= $old->{partitioner};
bless {
partitioner => $partitioner,
subs => [
($old ? @{ $old->{subs} } : ()),
[$type, $sub],
],
}, __PACKAGE__
}
sub once_by {
my ($self, $sub) = @_;
_new($self, "once", $sub);
}
sub then_by {
my ($self, $sub) = @_;
_new($self, "then", $sub);
}
sub refine {
my ($self) = @_;
unless (@{ $self->{subs} }) {
warn "Attempt to refine partitions without active refiners";
return;
}
my $old_size = $self->{partitioner}->size();
my $next_id = $old_size;
for (my $ix = 0; $ix < @{ $self->{subs} }; ++$ix) {
my @temp;
for my $sublist (@{ $self->{partitioner}{sublists} }) {
my %h;
for my $item (@{ $sublist }) {
local $_ = $item;
my $key = $self->{subs}[$ix][1]->($item);
push @{ $h{$key} }, $item;
}
push @temp, values %h;
}
#################################################################
#
#################################################################
my %occupied;
my @new_list;
for (my $ix = 0; $ix < @temp; ++$ix) {
my $first = $temp[$ix]->[0];
my $first_id = $self->{partitioner}->partition_of($first) // 0;
if (not $occupied{ $first_id }++) {
$new_list[ $first_id ] = $temp[$ix];
next;
}
my $new_id = $next_id++;
$new_list[ $new_id ] = $temp[$ix];
$self->{partitioner}{by_string}{$_} = $new_id
for @{ $temp[$ix] };
}
$self->{partitioner}{sublists} = \@new_list;
#################################################################
#
#################################################################
splice @{ $self->{ subs } }, $ix--, 1
if $self->{subs}[$ix]->[0] eq 'once';
}
return $self->{partitioner}->size() != $old_size;
}
1;
__END__
=head1 NAME
Acme::Partitioner - Iterated partition refinement.
=head1 SYNOPSIS
use Acme::Partitioner;
my $p = Acme::Partitioner->using(@states);
my $partitioner =
$p->once_by(sub { $dfa->is_accepting($_) })
->then_by(sub {
join " ", map { $p->partition_of($_) }
$dfa->transitions_from($_)
});
while ($partitioning->refine) {
say "Still partitioning, got "
. $p->size . " partitions so far";
}
=head1 DESCRIPTION
This module provides a simple interface to partition items of a set
into smaller sets based on criteria supplied by the caller. One step
in the refinement process extracts keys from the elements and groups
elements based on all of them. Criteria can be based on assignments
to partitions based on previous refinements, in which case multiple
refinements are necessary before the process stabilises.
=head2 METHODS
=over
=item Acme::Partitioner->using(@items)
Constructor, takes a list of items to be partitioned into clusters.
=item once_by($sub)
Constructs an object that C<refine> can be called on; takes a sub
routine that is expected to return a grouping key when called with
an item from the input list as argument. The sub will be called
only during the first refinement and not during later refinements.
Can also be called on objects returned by C<once_by>.
=item then_by($sub)
Similar to C<once_by> but the sub routine will always be called
during refinement.
=item refine
Refines the partitions. Returns a true value if further refinement
has been achieved, false if the number of partitions stayed the
same throughout refinement.
=item partition_of($item)
Numeric partition identifier for the supplied item.
=item items_in($partition)
Returns a list of all items in the supplied partition;
=item size()
Returns the current number of partitions.
=item all_partitions
Returns a list of lists of all partitions.
=back
=head2 EXPORTS
None.
=head2 SEE ALSO
=over
=item * L<http://en.wikipedia.org/wiki/Partition_refinement>
=back
=head1 AUTHOR / COPYRIGHT / LICENSE
Copyright (c) 2014 Bjoern Hoehrmann <bjoern@hoehrmann.de>.
This module is licensed under the same terms as Perl itself.
=cut