package Set::Functional; use 5.006; use Exporter qw{import}; =head1 NAME Set::Functional - set operations for functional programming =head1 VERSION Version 1.0 =cut our $VERSION = '1.0'; our @EXPORT_OK = qw{ setify setify_by cartesian difference difference_by disjoint disjoint_by distinct distinct_by intersection intersection_by symmetric_difference symmetric_difference_by union union_by is_disjoint is_disjoint_by is_equal is_equal_by is_equivalent is_equivalent_by is_pairwise_disjoint is_pairwise_disjoint_by is_proper_subset is_proper_subset_by is_proper_superset is_proper_superset_by is_subset is_subset_by is_superset is_superset_by }; our %EXPORT_TAGS = (all => \@EXPORT_OK); =head1 SYNOPSIS This module provides basic set operations for native lists. The primary goal is to take advantage of Perl's native functional programming capabilities while relying solely on Pure Perl constructs to perform the set operations as fast as possible. All of these techniques have been benchmarked against other common Perl idioms to determine the optimal solution. These benchmarks can be found in this package (shortly). Each function is provided in two forms. The first form always expects simple flat data structures of defined elements. The second form expects a BLOCK to evaluate each member of the list to a defined value to determine how the element is a set member. These can be identified by the suffix "_by". None of these functions check definedness inline so as to eliminate the costly O(n) operation. All functions have been prototyped to give them a native Perl-ish look and feel. Example usage: use Set::Functional ':all'; # Set Creation my @deduped_numbers = setify(1 .. 10, 2 .. 11); my @deduped_objects_by_name = setify_by { $_->{name} } ({name => 'fred'}, {name => 'bob'}, {name => 'fred'}); # Set Operation my @all_permutations = cartesian \@arr1, \@arr2, \@arr3, \@arr4; my @only_arr1_elements = difference \@arr1, \@arr2, \@arr3, \@arr4; my @only_arr1_elements_by_name = difference_by { $_->{name} } \@arr1, \@arr2, \@arr3, \@arr4; my @unique_per_set = disjoint \@arr1, \@arr2, \@arr3, \@arr4; my @unique_per_set_by_name = disjoint_by { $_->{name} } \@arr1, \@arr2, \@arr3, \@arr4; my @unique_elements = distinct \@arr1, \@arr2, \@arr3, \@arr4; my @unique_elements_by_name = distinct_by { $_->{name} } \@arr1, \@arr2, \@arr3, \@arr4; my @shared_elements = intersection \@arr1, \@arr2, \@arr3, \@arr4; my @shared_elements_by_name = intersection_by { $_->{name} } \@arr1, \@arr2, \@arr3, \@arr4; my @odd_occuring_elements = symmetric_difference \@arr1, \@arr2, \@arr3, \@arr4; my @odd_occuring_elements_by_name = symmetric_difference_by { $_->{name} } \@arr1, \@arr2, \@arr3, \@arr4; my @all_elements = union \@arr1, \@arr2, \@arr3, \@arr4; my @all_elements_by_name = union_by { $_->{name} } \@arr1, \@arr2, \@arr3, \@arr4; # Set Predicates my $is_all_of_arr1_distinct_from_arr2 = is_disjoint \@arr1, \@arr2; my $is_all_of_arr1_distinct_from_arr2_by_name = is_disjoint_by { $_->{name} } \@arr1, \@arr2; my $is_arr1_the_same_as_arr2 = is_equal \@arr1, \@arr2; my $is_arr1_the_same_as_arr2_by_name = is_equal_by { $_->{name} } \@arr1, \@arr2; my $are_all_sets_mutually_unique = is_pairwise_disjoint \@arr1, \@arr2, \@arr3, \@arr4; my $are_all_sets_mutually_unique_by_name = is_pairwise_disjoint_by { $_->{name} } \@arr1, \@arr2, \@arr3, \@arr4; my $is_all_of_arr1_in_arr2_but_not_the_same_as_arr2 = is_proper_subset \@arr1, \@arr2; my $is_all_of_arr1_in_arr2_but_not_the_same_as_arr2_by_name = is_proper_subset_by { $_->{name} } \@arr1, \@arr2; my $is_all_of_arr1_in_arr2 = is_subset \@arr1, \@arr2; my $is_all_of_arr1_in_arr2_by_name = is_subset_by { $_->{name} } \@arr1, \@arr2; my $is_all_of_arr2_in_arr1_but_not_the_same_as_arr1 = is_proper_superset \@arr1, \@arr2; my $is_all_of_arr2_in_arr1_but_not_the_same_as_arr1_by_name = is_proper_superset_by { $_->{name} } \@arr1, \@arr2; my $is_all_of_arr2_in_arr1 = is_superset \@arr1, \@arr2; my $is_all_of_arr2_in_arr1_by_name = is_superset_by { $_->{name} } \@arr1, \@arr2; =head1 CONSTRUCTORS =cut =head2 setify(@) Given a list, return a new set. Order is not guaranteed. setify 1 .. 10, 6 .. 15 => 1 .. 15 =cut sub setify(@) { my %set; undef @set{@_} if @_; return keys %set; } =head2 setify_by(&@) Given a choice function and a list, return a new set defined by the choice function. Order is not guaranteed. =cut sub setify_by(&@){ my $func = shift; my %set; @set{ map { $func->($_) } @_ } = @_ if @_; return values %set; } =head1 OPERATORS =cut =head2 cartesian(@) Given multiple set references, return multiple sets containing all permutations of one element from each set. If the empty set is provided, the empty set is returned. If no sets are provided then none are returned. cartesian [1 .. 3], [1 .. 2] => [1,1],[1,2],[2,1],[2,2],[3,1],[3,2] =cut sub cartesian(@) { return unless @_; my @results; my $repetitions = 1; ($repetitions *= @$_) || return [] for @_; $#results = $repetitions - 1; for my $idx (0 .. $#results) { $repetitions = @results; $results[$idx] = [map { $_->[int($idx/($repetitions /= @$_)) % @$_] } @_]; } return @results; } =head2 difference(@) Given multiple set references, return a new set with all the elements in the first set that don't exist in subsequent sets. difference [1 .. 10], [6 .. 15] => 1 .. 5 =cut sub difference(@) { my $first = shift; return unless $first && @$first; my %set; undef @set{@$first}; do { delete @set{@$_} if @$_ } for @_; return keys %set; } =head2 difference_by(&@) Given a choice function and multiple set references, return a new set with all the elements in the first set that don't exist in subsequent sets according to the choice function. =cut sub difference_by(&@) { my $func = shift; my $first = shift; return unless $first && @$first; my %set; @set{ map { $func->($_) } @$first } = @$first; do { delete @set{ map { $func->($_) } @$_ } if @$_ } for @_; return values %set; } =head2 disjoint(@) Given multiple set references, return corresponding sets containing all the elements from the original set that exist in any set exactly once. disjoint [1 .. 10], [6 .. 15] => [1 .. 5], [11 .. 15] =cut sub disjoint(@) { my %element_to_count; do { ++$element_to_count{$_} for @$_ } for @_; return map { [grep { $element_to_count{$_} == 1 } @$_] } @_; } =head2 disjoint_by(&@) Given a choice function and multiple set references, return corresponding sets containing all the elements from the original set that exist in any set exactly once according to the choice function. =cut sub disjoint_by(&@) { my $func = shift; my %key_to_count; do { ++$key_to_count{$func->($_)} for @$_ } for @_; return map { [grep { $key_to_count{$func->($_)} == 1 } @$_] } @_; } =head2 distinct(@) Given multiple set references, return a new set containing all the elements that exist in any set exactly once. distinct [1 .. 10], [6 .. 15] => 1 .. 5, 11 .. 15 =cut sub distinct(@) { my %element_to_count; do { ++$element_to_count{$_} for @$_ } for @_; return grep { $element_to_count{$_} == 1 } keys %element_to_count; } =head2 distinct_by(&@) Given a choice function and multiple set references, return a new set containing all the elements that exist in any set exactly once according to the choice function. =cut sub distinct_by(&@) { my $func = shift; my %key_to_count; for (@_) { for (@$_) { my $key = $func->($_); $key_to_count{$key} = exists $key_to_count{$key} ? undef : $_; } } return grep { defined } values %key_to_count; } =head2 intersection(@) Given multiple set references, return a new set containing all the elements that exist in all sets. intersection [1 .. 10], [6 .. 15] => 6 .. 10 =cut sub intersection(@) { my $first = shift; return unless $first && @$first; my %set; undef @set{@$first}; for (@_) { my @int = grep { exists $set{$_} } @$_; return unless @int; %set = (); undef @set{@int}; } return keys %set; } =head2 intersection_by(&@) Given a choice function and multiple set references, return a new set containing all the elements that exist in all sets according to the choice function. =cut sub intersection_by(&@) { my $func = shift; my $first = shift; return unless $first && @$first; my %set; @set{ map { $func->($_) } @$first } = @$first; for (@_) { my @int = grep { exists $set{$func->($_)} } @$_; return unless @int; %set = (); @set{ map { $func->($_) } @int } = @int; } return values %set; } =head2 symmetric_difference(@) Given multiple set references, return a new set containing all the elements that exist an odd number of times across all sets. symmetric_difference [1 .. 10], [6 .. 15], [4, 8, 12] => 1 .. 5, 8, 11 .. 15 =cut sub symmetric_difference(@) { my $count; my %element_to_count; do { ++$element_to_count{$_} for @$_ } for @_; return grep { $element_to_count{$_} % 2 } keys %element_to_count; } =head2 symmetric_difference_by(&@) Given a choice function and multiple set references, return a new set containing all the elements that exist an odd number of times across all sets according to the choice function. =cut sub symmetric_difference_by(&@) { my $func = shift; my $count; my %key_to_count; do { ++$key_to_count{$func->($_)} for @$_ } for @_; return map { grep { $count = delete $key_to_count{$func->($_)}; defined($count) && $count % 2 } @$_ } @_; } =head2 union(@) Given multiple set references, return a new set containing all the elements that exist in any set. union [1 .. 10], [6 .. 15] => 1 .. 15 =cut sub union(@) { my %set; do { undef @set{@$_} if @$_ } for @_; return keys %set; } =head2 union_by(&@) Given a choice function and multiple set references, return a new set containing all the elements that exist in any set according to the choice function. =cut sub union_by(&@) { my $func = shift; my %set; do { @set{ map { $func->($_) } @$_ } = @$_ if @$_ } for @_; return values %set; } =head1 PREDICATES =cut =head2 is_disjoint($$) Given two set references, return true if both sets contain none of the same values. is_disjoint [1 .. 5], [6 .. 10] => true is_disjoint [1 .. 6], [4 .. 10] => false =cut sub is_disjoint($$) { my @set = &intersection(@_[0,1]); return ! @set; } =head2 is_disjoint_by(&$$) Given a choice function and two sets references, return true if both sets contain none of the same values according to the choice function. =cut sub is_disjoint_by(&$$) { my @set = &intersection_by(@_[0,1,2]); return ! @set; } =head2 is_equal($$) Given two set references, return true if both sets contain all the same values. Aliased by is_equivalent. is_equal [1 .. 5], [1 .. 5] => true is_equal [1 .. 10], [6 .. 15] => false =cut sub is_equal($$) { my @set = &intersection(@_[0,1]); return @set == @{$_[0]} && @set == @{$_[1]}; } *is_equivalent = \&is_equal; =head2 is_equal_by(&$$) Given a choice function and two sets references, return true if both sets contain all the same values according to the choice function. Aliased by is_equivalent_by. =cut sub is_equal_by(&$$) { my @set = &intersection_by(@_[0,1,2]); return @set == @{$_[1]} && @set == @{$_[2]}; } *is_equivalent_by = \&is_equal_by; =head2 is_pairwise_disjoint(@) Given multiple set references, return true if every set is disjoint from every other set. is_pairwise_disjoint [1 .. 5], [6 .. 10], [11 .. 15] => true is_pairwise_disjoint [1 .. 5], [6 .. 10], [11 .. 15], [3 .. 8] => false =cut sub is_pairwise_disjoint(@) { my @sets = &disjoint(@_); do { return 0 if @{$sets[$_]} != @{$_[$_]} } for 0 .. $#sets; return 1; } =head2 is_pairwise_disjoint_by(&@) Given a choice function and multiple set references, return true if every set is disjoint from every other set according to the choice function. =cut sub is_pairwise_disjoint_by(&@) { my @sets = &disjoint_by((shift), @_); do { return 0 if @{$sets[$_]} != @{$_[$_]} } for 0 .. $#sets; return 1; } =head2 is_proper_subset($$) Given two set references, return true if the first set is fully contained by but is not equivalent to the second. is_proper_subset [1 .. 5], [1 .. 10] => true is_proper_subset [1 .. 5], [1 .. 5] => false =cut sub is_proper_subset($$) { my @set = &intersection(@_[0,1]); return @set == @{$_[0]} && @set != @{$_[1]}; } =head2 is_proper_subset_by(&$$) Given a choice function and two set references, return true if the first set is fully contained by but is not equivalent to the second according to the choice function. =cut sub is_proper_subset_by(&$$) { my @set = &intersection_by(@_[0,1,2]); return @set == @{$_[1]} && @set != @{$_[2]}; } =head2 is_proper_superset($$) Given two set references, return true if the first set fully contains but is not equivalent to the second. is_proper_superset [1 .. 10], [1 .. 5] => true is_proper_superset [1 .. 5], [1 .. 5] => false =cut sub is_proper_superset($$) { my @set = &intersection(@_[0,1]); return @set != @{$_[0]} && @set == @{$_[1]}; } =head2 is_proper_superset_by(&$$) Given a choice function and two set references, return true if the first set fully contains but is not equivalent to the second according to the choice function. =cut sub is_proper_superset_by(&$$) { my @set = &intersection_by(@_[0,1,2]); return @set != @{$_[1]} && @set == @{$_[2]}; } =head2 is_subset($$) Given two set references, return true if the first set is fully contained by the second. is_subset [1 .. 5], [1 .. 10] => true is_subset [1 .. 5], [1 .. 5] => true is_subset [1 .. 5], [2 .. 11] => false =cut sub is_subset($$) { my @set = &intersection(@_[0,1]); return @set == @{$_[0]}; } =head2 is_subset_by(&$$) Given a choice function and two set references, return true if the first set is fully contained by the second according to the choice function. =cut sub is_subset_by(&$$) { my @set = &intersection_by(@_[0,1,2]); return @set == @{$_[1]}; } =head2 is_superset($$) Given two set references, return true if the first set fully contains the second. is_superset [1 .. 10], [1 .. 5] => true is_superset [1 .. 5], [1 .. 5] => true is_subset [1 .. 5], [2 .. 11] => false =cut sub is_superset($$) { my @set = &intersection(@_[0,1]); return @set == @{$_[1]}; } =head2 is_superset_by(&$$) Given a choice function and two set references, return true if the first set fully contains the second according to the choice function. =cut sub is_superset_by(&$$) { my @set = &intersection_by(@_[0,1,2]); return @set == @{$_[2]}; } =head1 AUTHOR Aaron Cohen, C<< >> Special thanks to: L L =head1 BUGS Please report any bugs or feature requests to C, or through the web interface at L. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes. =head1 TODO =over 4 =item * Add SEE ALSO section =back =head1 SUPPORT You can find documentation for this module with the perldoc command. perldoc Set::Functional You can also look for information at: =over 4 =item * Official GitHub Repo L =item * GitHub's Issue Tracker (report bugs here) L =item * CPAN Ratings L =item * Official CPAN Page L =back =head1 LICENSE AND COPYRIGHT Copyright 2011-2013 Aaron Cohen. This program is free software; you can redistribute it and/or modify it under the terms of either: the GNU General Public License as published by the Free Software Foundation; or the Artistic License. See http://dev.perl.org/licenses/ for more information. =cut 1; # End of Set::Functional