oops, I was testing with the wrong perl earlier, reverted one optimization that does...
[gitmo/Algorithm-C3.git] / lib / Algorithm / C3.pm
CommitLineData
c0b91998 1
2package Algorithm::C3;
3
4use strict;
5use warnings;
6
7use Carp 'confess';
8
21f53c4c 9our $VERSION = '0.06';
c0b91998 10
c0b91998 11sub merge {
ca604ce2 12 my ($root, $parent_fetcher, $cache) = @_;
cf85d7d3 13
ca604ce2 14 $cache ||= {};
cf85d7d3 15 my @STACK; # stack for simulating recursion
cf85d7d3 16
aeed4a60 17 my $pfetcher_is_coderef = ref($parent_fetcher) eq 'CODE';
18
19 unless ($pfetcher_is_coderef or $root->can($parent_fetcher)) {
20 confess "Could not find method $parent_fetcher in $root";
21 }
22
cf85d7d3 23 my $current_root = $root;
aeed4a60 24 my $current_parents = [ $root->$parent_fetcher ];
cf85d7d3 25 my $recurse_mergeout = [];
26 my $i = 0;
0f7ef7b1 27 my %seen = ( $root => 1 );
cf85d7d3 28
f31aa6f6 29 my ($new_root, $mergeout, %tails);
cf85d7d3 30 while(1) {
31 if($i < @$current_parents) {
403ea967 32 $new_root = $current_parents->[$i++];
fa27b316 33
34 if($seen{$new_root}) {
0f7ef7b1 35 my @isastack = (
36 (map { $_->[0] } @STACK),
37 $current_root,
38 $new_root
39 );
40 shift @isastack while $isastack[0] ne $new_root;
41 my $isastack = join(q{ -> }, @isastack);
42 die "Infinite loop detected in parents of '$root': $isastack";
fa27b316 43 }
bb0280c5 44 $seen{$new_root} = 1;
cf85d7d3 45
aeed4a60 46 unless ($pfetcher_is_coderef or $new_root->can($parent_fetcher)) {
47 confess "Could not find method $parent_fetcher in $new_root";
48 }
49
cf85d7d3 50 push(@STACK, [
51 $current_root,
52 $current_parents,
53 $recurse_mergeout,
54 $i,
55 ]);
56
57 $current_root = $new_root;
ca604ce2 58 $current_parents = $cache->{pfetch}->{$current_root} ||= [ $current_root->$parent_fetcher ];
cf85d7d3 59 $recurse_mergeout = [];
60 $i = 0;
61 next;
62 }
63
bb0280c5 64 $seen{$current_root} = 0;
65
403ea967 66 $mergeout = $cache->{merge}->{$current_root} ||= do {
cf85d7d3 67
68 # This do-block is the code formerly known as the function
69 # that was a perl-port of the python code at
70 # http://www.python.org/2.3/mro.html :)
71
7946639b 72 # Initial set (make sure everything is copied - it will be modded)
403ea967 73 my @seqs;
74 foreach (@$recurse_mergeout, $current_parents) {
75 push(@seqs, [@$_]) if @$_;
76 }
5a07d049 77
f31aa6f6 78 # Construct the tail-checking hash (actually, it's cheaper and still
79 # correct to re-use it throughout this function)
80 foreach my $seq (@seqs) {
403ea967 81 $tails{$seq->[$_]}++ for (1..$#$seq);
cf85d7d3 82 }
83
7946639b 84 my @res = ( $current_root );
cf85d7d3 85 while (1) {
5a07d049 86 my $cand;
87 my $winner;
cf85d7d3 88 foreach (@seqs) {
5a07d049 89 next if !@$_;
90 if(!$winner) { # looking for a winner
91 $cand = $_->[0]; # seq head is candidate
92 next if $tails{$cand}; # he loses if in %tails
fc8171f5 93
94 # Handy warn to give a output like the ones on
95 # http://www.python.org/download/releases/2.3/mro/
96 #warn " = " . join(' + ', @res) . " + merge([" . join('] [', map { join(', ', @$_) } grep { @$_ } @seqs) . "])\n";
5a07d049 97 push @res => $winner = $cand;
403ea967 98 shift @$_; # strip off our winner
99 $tails{$_->[0]}-- if @$_; # keep %tails sane
cf85d7d3 100 }
403ea967 101 elsif($_->[0] eq $winner) {
cf85d7d3 102 shift @$_; # strip off our winner
5a07d049 103 $tails{$_->[0]}-- if @$_; # keep %tails sane
cf85d7d3 104 }
105 }
fc8171f5 106
107 # Handy warn to give a output like the ones on
108 # http://www.python.org/download/releases/2.3/mro/
109 #warn " = " . join(' + ', @res) . "\n" if !$cand;
110
cf85d7d3 111 last if !$cand;
112 die q{Inconsistent hierarchy found while merging '}
113 . $current_root . qq{':\n\t}
114 . qq{current merge results [\n\t\t}
115 . (join ",\n\t\t" => @res)
116 . qq{\n\t]\n\t} . qq{merging failed on '$cand'\n}
117 if !$winner;
118 }
119 \@res;
120 };
121
122 return @$mergeout if !@STACK;
123
124 ($current_root, $current_parents, $recurse_mergeout, $i)
125 = @{pop @STACK};
126
127 push(@$recurse_mergeout, $mergeout);
128 }
c0b91998 129}
130
1311;
132
133__END__
134
135=pod
136
137=head1 NAME
138
8fe16bec 139Algorithm::C3 - A module for merging hierarchies using the C3 algorithm
c0b91998 140
141=head1 SYNOPSIS
142
143 use Algorithm::C3;
144
145 # merging a classic diamond
146 # inheritence graph like this:
147 #
148 # <A>
149 # / \
150 # <B> <C>
151 # \ /
152 # <D>
153
154 my @merged = Algorithm::C3::merge(
155 'D',
156 sub {
157 # extract the ISA array
158 # from the package
159 no strict 'refs';
160 @{$_[0] . '::ISA'};
161 }
162 );
163
164 print join ", " => @merged; # prints D, B, C, A
165
166=head1 DESCRIPTION
167
168This module implements the C3 algorithm. I have broken this out
169into it's own module because I found myself copying and pasting
170it way too often for various needs. Most of the uses I have for
171C3 revolve around class building and metamodels, but it could
172also be used for things like dependency resolution as well since
173it tends to do such a nice job of preserving local precendence
174orderings.
175
176Below is a brief explanation of C3 taken from the L<Class::C3>
177module. For more detailed information, see the L<SEE ALSO> section
178and the links there.
179
180=head2 What is C3?
181
182C3 is the name of an algorithm which aims to provide a sane method
183resolution order under multiple inheritence. It was first introduced
184in the langauge Dylan (see links in the L<SEE ALSO> section), and
185then later adopted as the prefered MRO (Method Resolution Order)
186for the new-style classes in Python 2.3. Most recently it has been
187adopted as the 'canonical' MRO for Perl 6 classes, and the default
188MRO for Parrot objects as well.
189
190=head2 How does C3 work.
191
192C3 works by always preserving local precendence ordering. This
193essentially means that no class will appear before any of it's
194subclasses. Take the classic diamond inheritence pattern for
195instance:
196
197 <A>
198 / \
199 <B> <C>
200 \ /
201 <D>
202
203The standard Perl 5 MRO would be (D, B, A, C). The result being that
204B<A> appears before B<C>, even though B<C> is the subclass of B<A>.
205The C3 MRO algorithm however, produces the following MRO (D, B, C, A),
206which does not have this same issue.
207
208This example is fairly trival, for more complex examples and a deeper
209explaination, see the links in the L<SEE ALSO> section.
210
211=head1 FUNCTION
212
213=over 4
214
ca604ce2 215=item B<merge ($root, $func_to_fetch_parent, $cache)>
c0b91998 216
217This takes a C<$root> node, which can be anything really it
218is up to you. Then it takes a C<$func_to_fetch_parent> which
219can be either a CODE reference (see L<SYNOPSIS> above for an
220example), or a string containing a method name to be called
221on all the items being linearized. An example of how this
222might look is below:
223
224 {
225 package A;
226
227 sub supers {
228 no strict 'refs';
229 @{$_[0] . '::ISA'};
230 }
231
232 package C;
233 our @ISA = ('A');
234 package B;
235 our @ISA = ('A');
236 package D;
237 our @ISA = ('B', 'C');
238 }
239
240 print join ", " => Algorithm::C3::merge('D', 'supers');
241
242The purpose of C<$func_to_fetch_parent> is to provide a way
243for C<merge> to extract the parents of C<$root>. This is
244needed for C3 to be able to do it's work.
245
ca604ce2 246The C<$cache> parameter is an entirely optional performance
247measure, and should not change behavior.
248
249If supplied, it should be a hashref that merge can use as a
250private cache between runs to speed things up. Generally
251speaking, if you will be calling merge many times on related
252things, and the parent fetching function will return constant
253results given the same arguments during all of these calls,
254you can and should reuse the same shared cache hash for all
255of the calls. Example:
256
257 sub do_some_merging {
258 my %merge_cache;
259 my @foo_mro = Algorithm::C3::Merge('Foo', \&get_supers, \%merge_cache);
260 my @bar_mro = Algorithm::C3::Merge('Bar', \&get_supers, \%merge_cache);
261 my @baz_mro = Algorithm::C3::Merge('Baz', \&get_supers, \%merge_cache);
262 my @quux_mro = Algorithm::C3::Merge('Quux', \&get_supers, \%merge_cache);
263 # ...
264 }
265
c0b91998 266=back
267
268=head1 CODE COVERAGE
269
270I use B<Devel::Cover> to test the code coverage of my tests, below
271is the B<Devel::Cover> report on this module's test suite.
272
273 ------------------------ ------ ------ ------ ------ ------ ------ ------
274 File stmt bran cond sub pod time total
275 ------------------------ ------ ------ ------ ------ ------ ------ ------
6d8a26f9 276 Algorithm/C3.pm 100.0 100.0 100.0 100.0 100.0 100.0 100.0
c0b91998 277 ------------------------ ------ ------ ------ ------ ------ ------ ------
6d8a26f9 278 Total 100.0 100.0 100.0 100.0 100.0 100.0 100.0
c0b91998 279 ------------------------ ------ ------ ------ ------ ------ ------ ------
280
281=head1 SEE ALSO
282
283=head2 The original Dylan paper
284
285=over 4
286
287=item L<http://www.webcom.com/haahr/dylan/linearization-oopsla96.html>
288
289=back
290
291=head2 The prototype Perl 6 Object Model uses C3
292
293=over 4
294
295=item L<http://svn.openfoundry.org/pugs/perl5/Perl6-MetaModel/>
296
297=back
298
299=head2 Parrot now uses C3
300
301=over 4
302
303=item L<http://aspn.activestate.com/ASPN/Mail/Message/perl6-internals/2746631>
304
305=item L<http://use.perl.org/~autrijus/journal/25768>
306
307=back
308
309=head2 Python 2.3 MRO related links
310
311=over 4
312
313=item L<http://www.python.org/2.3/mro.html>
314
315=item L<http://www.python.org/2.2.2/descrintro.html#mro>
316
317=back
318
319=head2 C3 for TinyCLOS
320
321=over 4
322
323=item L<http://www.call-with-current-continuation.org/eggs/c3.html>
324
325=back
326
934d071b 327=head1 AUTHORS
c0b91998 328
329Stevan Little, E<lt>stevan@iinteractive.comE<gt>
330
f4e5601f 331Brandon L. Black, E<lt>blblack@gmail.comE<gt>
934d071b 332
c0b91998 333=head1 COPYRIGHT AND LICENSE
334
335Copyright 2006 by Infinity Interactive, Inc.
336
337L<http://www.iinteractive.com>
338
339This library is free software; you can redistribute it and/or modify
340it under the same terms as Perl itself.
341
342=cut