The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Set::DynamicGroups - Manage groups of items dynamically

VERSION

version 0.014

SYNOPSIS

  use Set::DynamicGroups;

  my $set = Set::DynamicGroups->new();
  $set->add(group_name => 'member1');

  $set->add(green => ['junior', 'french peas']);
  $set->add(blue  => {include => 'madame blueberry'});
  $set->add(other => {not_in => [qw(blue green)]});

  my @members = $set->group('group_name');
  # or
  my $all = $set->groups();

DESCRIPTION

An instance of Set::DynamicGroups can manage a list of groups and the items (members) of those groups. It takes in various definitions of groups (rules about how to build the member list (see "GROUP SPECIFICATION")) and will return the list of items contained in any named groups.

The module was specifically designed to allow groups to be defined dynamically by rules based on other groups. For instance you can define one group as a list of all the items included in two other groups. You can also say that one group will be composed of any known members not in some other group.

METHODS

new

  my $set = Set::DynamicGroups->new();

Constructor.

Takes no arguments.

add

  $set->add(group_name => $group_spec);

Add items to the specified group.

See "GROUP SPECIFICATION" for details on the possible values of $group_spec.

add_items

  $set->add_items(qw(bob larry));
  $set->add_items('archibald', [qw(jimmy jerry)]);

Add the provided items to the full list of known items. Arguments can be strings or array references (which will be flattened).

This is useful to include items that you know are available but may not be explicitly included in other groups. Then groups defined by exclusions will base their members off of all known items.

Items that are specified in group definitions do not need to be specified separately.

Aliased as add_members.

group

  @items = $set->group($group_name);

Return a list of the items in the specified group.

This is a convenience method that calls "groups" with the provided group name and returns a list (rather than a hash of arrayrefs).

The above example is equivalent to:

  @items = @{ $set->groups($group_name)->{$group_name} };

except that it will croak if the specified group does not exist.

groups

  $set->groups(); # returns {groupname => \@items, ...}
  $set->groups(@group_names);

Return a hashref of each group and the items contained.

Sending a list of group names will restrict the hashref to just those groups (instead of all).

The keys are group names and the values are arrayrefs of items.

See "DEPENDENCY RESOLUTION" for a discussion on the way members are determined for mutually dependent groups.

items

  my @items = $set->items();

Return a list of all known items.

This includes any items specified explicitly with "add_items" as well all items explicitly included in group specifications.

Aliased as members.

normalize

  $norm_spec = $set->normalize($group_spec);

Used internally to normalize group specifications.

Upgrades a string to an arrayref. Upgrades an arrayref to a hash. Renames aliases to the canonical keys.

See "GROUP SPECIFICATION".

set

  $set->set(group_name => $group_spec);

Set a group specification to the provided value (resetting any previous specifications).

This is a shortcut for removing any previous specifications and then calling "add".

set_items

  $set->set_items(@items);

Set the full list of items to the provided items.

This is a shortcut for removing any previous items and then calling "add_items".

Aliased as set_members.

GROUP SPECIFICATION

A group specification can be in one of the following formats:

string

A single member; This is converted to an arrayref with one element.

arrayref

An array of items. This is converted into a hashref with the include key.

hashref

Possible options:

include (or items (or members))

An arrayref of items to include in the group

exclude (or not)

An arrayref of items to exclude from the group

include_groups (or in)

An arrayref of groups whose items will be included

exclude_groups (or not)

An arrayref of groups whose items will be excluded

Each option can be a an arrayref or a string which will be converted to an arrayref with a single element.

Specifications that only have exclude and/or exclude_groups will first be filled with all known items. (This is where "add_items" comes in.)

DEPENDENCY RESOLUTION

The main impetus for the design of this module was the desire to define groups dependent on the definition of other groups.

This appears to work for the limited test cases I have come up with.

However, mutually dependent groups present a problem.

(If you're not dealing with mutually dependent groups feel free to skip this section.)

In order to avoid infinite recursion when determining a group's members a dependency resolution strategy is needed.

I have not determined a canonical strategy, but imagine that multiple could be argued for, and perhaps an option/attribute on the object would be the most useful.

I do not have a use-case for mutually dependent groups, so I have put little thought (and even less code) into it.

What follows is the discussion I've had so far (with the two plush penguins on my desk):

Possible strategies:

  • die / croak / stop

    croak if a mutual dependency is found.

    Simple, but possibly not always the most helpful.

  • each / more

    Try to determine each group's members independently of any other groups. This often results in groups getting more members (than less).

      b => {in => 'c'}
      c => {not_in => 'b', include => 'cat'}
    
      # result:
      #   b => ['cat']
      #   c => ['cat']

    Why? If we start with b:

    • b will try to resolve c

    • c will include cat and then try to resolve b

    • Since b is already in the stack it cannot be resolved and returns []

    • c finishes as ['cat']

    • b included ['cat'] (from c)

    • b finishes as ['cat']

    Then C will try to resolve itself independently:

    • c will include cat and then try to resolve b

    • b will try to resolve c

    • Since c is already in the stack it cannot be resolved and returns []

    • b finished as []

    • c included ['cat'] and excluded [] (from b)

    • c finishes as ['cat']

    It may not seem quite right that b and c end up equaling each other, but honestly what would you expect from those definitions (besides infinite recursion)?

  • once? / less?

    Determine each group once rather than restarting for each group. This may involve passing the entire stack of resolutions-thus-far instead of just the names currently being resolved. I haven't really determined if this could work reliably or what exactly would happen.

  • includes_first

    First do the includes (the easy part) for each group, then go through them all again and try to resolve groups from what we have thus far.

    This might turn out differently than each, though I have not contemplated the actual implementation.

  • hard

    Try hard to determine the members for each group. Start with the includes, then make a stack of all the groups and process each group... If a group finishes successfully (rather than exiting early to avoid infinite recursion) remove it from the stack. Keep looping over the stack attempting to process each group until all have been removed or until a full loop through the stack removes none.

    Then resort to one of the other strategies to resolve any remaining groups.

The current implementation is each (more) because that is what I determined to be happening at my first attempt to stop the infinite recursion.

If you have ideas on strategies, implementations, or test cases feel free to send me your thoughts.

As always, patches are welcome.

BUGS AND LIMITATIONS

Possibly a lot if you get really complex with group dependencies. See "DEPENDENCY RESOLUTION" for the current discussion on the topic.

Currently everything is calculated upon request. This may be an important part of one of the dependency resolution strategies, but if at any time it is not, then it's merely inefficient.

RATIONALE

I searched for other "grouping" modules on CPAN but found none that supported basing one group off of another. Unsatisfied by the API of the modules I looked at, I borrowed their namespace and created this implementation.

SEE ALSO

SUPPORT

Perldoc

You can find documentation for this module with the perldoc command.

  perldoc Set::DynamicGroups

Websites

The following websites have more information about this module, and may be of help to you. As always, in addition to those websites please use your favorite search engine to discover more resources.

Bugs / Feature Requests

Please report any bugs or feature requests by email to bug-set-dynamicgroups at rt.cpan.org, or through the web interface at http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Set-DynamicGroups. You will be automatically notified of any progress on the request by the system.

Source Code

http://github.com/rwstauner/Set-DynamicGroups

  git clone http://github.com/rwstauner/Set-DynamicGroups

AUTHOR

Randy Stauner <rwstauner@cpan.org>

COPYRIGHT AND LICENSE

This software is copyright (c) 2010 by Randy Stauner.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.