NAME
Operator::Util - A selection of array and hash functions that extend operators
VERSION
This document describes Operator::Util version 0.03.
SYNOPSIS
use Operator::Util qw(
reduce reducewith
zip zipwith
cross crosswith
hyper hyperwith
applyop reverseop
);
WARNING
This is a pre-alpha release of Operator::Util. Not all features described in this document are complete. Please see the "TODO" list for details. The interface and functionality may change in the future based on user feedback. Please make suggestions by creating an issue at http://github.com/patch/operator-util-pm5/issues.
DESCRIPTION
A pragmatic approach at providing the functionality of many of Perl 6's meta-operators in Perl 5.
The terms "operator string" or "opstring" are used to describe a string that represents an operator, such as the string '+'
for the addition operator or the string '.'
for the concatenation operator. Opstrings default to binary infix operators and the short form may be used, e.g., '*'
instead of 'infix:*'
. All other operator types (prefix, postfix, circumfix, and postcircumfix) must have the type prepended in the opstrings, e.g., prefix:++
and postcircumfix:{}
.
When a list is passed as an argument for any of the functions, it must be either an array reference or a scalar value that will be used as a single-element list.
The following functions are provided but are not exported by default.
Reduction
- reduce OPSTRING, LIST [, triangle => 1 ]
-
reducewith
is an alias forreduce
. It may be desirable to usereducewith
to avoid naming conflicts or confusion with "reduce" in List::Util.Any infix opstring (except for non-associating operators) can be passed to
reduce
along with an arrayref to reduce the array using that operation:reduce('+', [1, 2, 3]) # 1 + 2 + 3 = 6 my @a = (5, 6) reduce('*', \@a) # 5 * 6 = 30
reduce
associates the same way as the operator used:reduce('-', [4, 3, 2]) # 4-3-2 = (4-3)-2 = -1 reduce('**', [4, 3, 2]) # 4**3**2 = 4**(3**2) = 262144
For comparison operators (like
<
), all reduced pairs of operands are broken out into groups and joined with&&
because Perl 5 doesn't support comparison operator chaining:reduce('<', [1, 3, 5]) # 1 < 3 && 3 < 5
If fewer than two elements are given, the results will vary depending on the operator:
reduce('+', []) # 0 reduce('+', [5]) # 5 reduce('*', []) # 1 reduce('*', [5]) # 5
If there is one element, the
reduce
returns that one element. However, this default doesn't make sense for operators like<
that don't return the same type as they take, so these kinds of operators overload the single-element case to return something more meaningful.You can also reduce the comma operator, although there isn't much point in doing so. This just returns an arrayref that compares deeply to the arrayref passed in:
[1, 2, 3] reduce(',', [1, 2, 3]) # same thing
Operators with zero-element arrayrefs return the following values:
** # 1 (arguably nonsensical) =~ # 1 (also for 1 arg) !~ # 1 (also for 1 arg) * # 1 / # fail (reduce is nonsensical) % # fail (reduce is nonsensical) x # fail (reduce is nonsensical) + # 0 - # 0 . # '' << # fail (reduce is nonsensical) >> # fail (reduce is nonsensical) < # 1 (also for 1 arg) > # 1 (also for 1 arg) <= # 1 (also for 1 arg) >= # 1 (also for 1 arg) lt # 1 (also for 1 arg) le # 1 (also for 1 arg) gt # 1 (also for 1 arg) ge # 1 (also for 1 arg) == # 1 (also for 1 arg) != # 1 (also for 1 arg) eq # 1 (also for 1 arg) ne # 1 (also for 1 arg) ~~ # 1 (also for 1 arg) & # -1 (from ^0, the 2's complement in arbitrary precision) | # 0 ^ # 0 && # 1 || # 0 // # 0 = # undef (same for all assignment operators) , # []
You can say
reduce('||', [a(), b(), c(), d()])
to return the first true result, but the evaluation of the list is controlled by the semantics of the list, not the semantics of
||
.To generate all intermediate results along with the final result, you can set the
triangle
argument:reduce('+', [1..5], triangle=>1) # (1, 3, 6, 10, 15)
The visual picture of a triangle is not accidental. To produce a triangular list of lists, you can use a "triangular comma":
reduce(',', [1..5], triangle=>1) # [1], # [1,2], # [1,2,3], # [1,2,3,4], # [1,2,3,4,5]
Zip
- zipwith OPSTRING, LIST1, LIST2
- zip LIST1, LIST2
-
The
zipwith
function may be passed any infix opstring. It applies the operator across all groupings of its list elements.The string concatenating form is:
zipwith('.', ['a','b'], [1,2]) # ('a1', 'b2')
The list concatenating form when used like this:
zipwith(',', ['a','b'], [1,2], ['x','y'])
produces
('a', 1, 'x', 'b', 2, 'y')
This list form is common enough to have a shortcut, calling
zip
without an opstring as the first argument will use,
by default:zip(['a','b'], [1,2], ['x','y'])
also produces
('a', 1, 'x', 'b', 2, 'y')
Any non-mutating infix operator may be used.
zipwith('*', [1,2], [3,4]) # (3, 8)
All assignment operators are considered mutating.
If the underlying operator is non-associating, so is
zipwith
, except for basic comparison operators since a chaining workaround is provided:zipwith('cmp', \@a, \@b, \@c) # ILLEGAL zipwith('eq', \@a, \@b, \@c) # ok
The underlying operator is always applied with its own associativity, just as the corresponding
reduce
operator would do.All lists are assumed to be flat; multidimensional lists are handled by treating the first dimension as the only dimension.
The response is a flat list by default. To return a list of arrayrefs, unset the
flat
argument:zip(['a','b'], [1,2], ['x','y'], flat=>0)
produces:
(['a', 1, 'x'], ['b', 2, 'y'])
Cross
- crosswith OPSTRING, LIST1, LIST2
- cross LIST1, LIST2
-
The
crosswith
function may be passed any infix opstring. It applies the operator across all groupings of its list elements.The string concatenating form is:
crosswith('.', ['a','b'], [1,2]) # ('a1', 'a2', 'b1', 'b2')
The list concatenating form when used like this:
crosswith(',', ['a','b'], [1,2], ['x','y'])
produces
'a', 1, 'x', 'a', 1, 'y', 'a', 2, 'x', 'a', 2, 'y', 'b', 1, 'x', 'b', 1, 'y', 'b', 2, 'x', 'b', 2, 'y'
This list form is common enough to have a shortcut, calling
cross
without an opstring as the first argument will use,
by default:cross(['a','b'], [1,2], ['x','y'])
Any non-mutating infix operator may be used.
crosswith('*', [1,2], [3,4]) # (3, 4, 6, 8)
All assignment operators are considered mutating.
If the underlying operator is non-associating, so is
crosswith
, except for basic comparison operators since a chaining workaround is provided:crosswith('cmp', \@a, \@b, \@c) # ILLEGAL crosswith('eq', \@a, \@b, \@c) # ok
The underlying operator is always applied with its own associativity, just as the corresponding
reduce
operator would do.All lists are assumed to be flat; multidimensional lists are handled by treating the first dimension as the only dimension.
The response is a flat list by default. To return a list of arrayrefs, unset the
flat
argument:cross(['a','b'], [1,2], ['x','y'], flat=>0)
produces:
['a', 1, 'x'], ['a', 1, 'y'], ['a', 2, 'x'], ['a', 2, 'y'], ['b', 1, 'x'], ['b', 1, 'y'], ['b', 2, 'x'], ['b', 2, 'y']
Hyper
- hyper OPSTRING, LIST1, LIST2 [, dwim_left => 1, dwim_right => 1 ]
- hyper OPSTRING, LIST
-
hyperwith
is an alias forhyper
.The
hyper
function operates on each element of its arrayref argument (or arguments) and returns a single list of the results. In other words,hyper
distributes the operator over its elements as lists.hyper('prefix:-' [1,2,3]) # (-1,-2,-3) hyper('+', [1,1,2,3,5], [1,2,3,5,8]) # (2,3,5,8,13)
Unary operators always produce a list of exactly the same shape as their single argument. When infix operators are presented with two arrays of identical shape, a result of that same shape is produced. Otherwise the result depends on what
dwim
arguments are passed.For an infix operator, if either argument is insufficiently dimensioned,
hyper
"upgrades" it, but only if you tell it to "dwim" on that side.hyper('-', [3,8,2,9,3,8], 1, dwim_right=>1) # (2,7,1,8,2,7) hyper('+=', \@array, 42, dwim_right=>1) # add 42 to each element
If you don't know whether one side or the other will be under-dimensioned, you can dwim on both sides:
hyper('*', $left, $right, dwim=>1)
The upgrade never happens on the non-dwim end of a
hyper
. If you writehyper('*', $bigger, $smaller, dwim_left=>1) hyper('*', $smaller, $bigger, dwim_right=>1)
an exception is thrown, and if you write
hyper('*', $foo, $bar)
you are requiring the shapes to be identical, or an exception will be thrown.
For all hyper dwimminess, if a scalar is found where the other side expects an array, the scalar is considered to be an array of one element.
Once we have two lists to process, we have to decide how to put the elements into correspondence. If both sides are dwimmy, the short list will have to be repeated as many times as necessary to make the appropriate number of elements.
If only one side is dwimmy, then the list on that side only will be grown or truncated to fit the list on the non-dwimmy side.
This produces an array the same length as the corresponding dimension on the other side. The original operator is then recursively applied to each corresponding pair of elements, in case there are more dimensions to handle.
Here are some examples:
hyper('+', [1,2,3,4], [1,2] ) # always error hyper('+', [1,2,3,4], [1,2], dwim=>1 ) # (2,4,4,6) rhs dwims to 1,2,1,2 hyper('+', [1,2,3], [1,2], dwim=>1 ) # (2,4,4) rhs dwims to 1,2,1 hyper('+', [1,2,3,4], [1,2], dwim_left=>1 ) # (2,4) lhs dwims to 1,2 hyper('+', [1,2,3,4], [1,2], dwim_right=>1) # (2,4,4,6) rhs dwims to 1,2,1,2 hyper('+', [1,2,3], [1,2], dwim_right=>1) # (2,4,4) rhs dwims to 1,2,1 hyper('+', [1,2,3], 1, dwim_right=>1) # (2,3,4) rhs dwims to 1,1,1
Another way to look at it is that the dwimmy array's elements are indexed modulo its number of elements so as to produce as many or as few elements as necessary.
Note that each element of a dwimmy list may in turn be expanded into another dimension if necessary, so you can, for instance, add one to all the elements of a matrix regardless of its dimensionality:
hyper('+=', \@fancy, 1, dwim_right=>1)
On the non-dwimmy side, any scalar value will be treated as an array of one element, and for infix operators must be matched by an equivalent one-element array on the other side. That is,
hyper
is guaranteed to degenerate to the corresponding scalar operation when all its arguments are non-array arguments.When using a unary operator no dwimmery is ever needed:
@negatives = hyper('prefix:-', \@positives) hyper('postfix:++', \@positions) # increment each hyper('->', \@objects, 'run', dwim_right=>1) # call ->run() on each hyper('length', ['f','oo','bar']) # (1, 2, 3)
Note that method calls are infix operators with a string used for the method name.
Hyper operators are defined recursively on nested arrays, so:
hyper('prefix:-', [[1, 2], 3]) # ([-1, -2], -3)
Likewise the dwimminess of dwimmy infixes propagates:
hyper('+', [[1, 2], 3], [4, [5, 6]], dwim=>1) # [[5, 6], [8, 9]]
hyper
may be applied to hashes as well as to arrays. In this case "dwimminess" says whether to ignore keys that do not exist in the other hash, while "non-dwimminess" says to use all keys that are in either hash. That is,hyper('+', \%foo, \%bar, dwim=>1)
gives you the intersection of the keys, while
hyper('+', \%foo, \%bar)
gives you the union of the keys. Asymmetrical hypers are also useful; for instance, if you say:
hyper('+', \%outer, \%inner, dwim_right=>1)
only the %inner keys that already exist in %outer will occur in the result. Note, however, that you want
hyper('+=', \%outer, \%inner)
in order to pass accumulated statistics up a tree, assuming you want %outer to have the union of keys.
Unary hash hypers and binary hypers that have only one hash operand will apply the hyper operator to just the values but return a new hash value with the same set of keys as the original hash.
hyper('prefix:-' {a => 1, b => 2, c => 3}) # (a => -1, b => -2, c => -3)
Flat list vs. "list of lists"
The optional named-argument flat
can be passed to any of the above functions. It defaults to 1
, which causes the function to return a flat list. When set to 0
, it causes the return value from each operator to be stored in an array ref, resulting in a "list of lists" being returned from the function.
zip([1..3], ['a'..'c']) # 1, 'a', 2, 'b', 3, 'c'
zip([1..3], ['a'..'c'], flat=>0) # [1, 'a'], [2, 'b'], [3, 'c']
Other utils
- applyop OPSTRING, OPERAND1, OPERAND2
- applyop OPSTRING, OPERAND
-
Apply the operator OPSTRING to the operands OPERAND1 and OPERAND2. If an unary opstring is provided, only the first operand will be used.
applyop('.', 'foo', 'bar') # foobar applyop('prefix:++', 5) # 6
- reverseop OPSTRING, OPERAND1, OPERAND2
-
reverseop
provides the same functionality asapplyop
except that OPERAND1 and OPERAND2 are reversed.reverseop('.', 'foo', 'bar') # barfoo
If an unary opstring is used,
reverseop
has the same functionality asapplyop
.
TODO
Allow more than two arrayrefs with
zipwith
,crosswith
, andhyper
Support multi-dimensional binary operator distribution with
hyper
Support the
flat => 0
optionAdd
warn
ings on errors instead of simplyreturn
ingAdd named unary operators such as
uc
andlc
Support meta-operator literals such as
Z
andX
inapplyop
Add
evalop
foreval
ing strings including meta-operator literalsShould the first argument optionally be a subroutine ref instead of an operator string?
Should the
flat => 0
option be changed tolol => 1
?
SEE ALSO
"pairwise" in List::MoreUtils is similar to
zip
except that its first argument is a block instead of an operator string and the remaining arguments are arrays instead of array refs:pairwise { $a + $b }, @array1, @array2 # List::MoreUtils zip '+', \@array1, \@array2 # Operator::Util
mesh
a.k.a. "zip" in List::MoreUtils is similar tozip
except that the arguments are arrays instead of array refs:mesh @array1, @array2 # List::MoreUtils zip \@array1, \@array2 # Operator::Util
Set::CrossProduct is an object-oriented alternative to
cross
The "Meta operators" section of Synopsis 3: Perl 6 Operators (http://perlcabal.org/syn/S03.html#Meta_operators) is the inspiration for this module
AUTHOR
Nick Patch <patch@cpan.org>
ACKNOWLEDGEMENTS
This module is based on the Perl 6 specification, as described in the Synopsis and implemented in Rakudo
Much of the documentation is taken directly from Synopsis 3: Perl 6 Operators (http://perlcabal.org/syn/S03.html)
The tests were forked from the Official Perl 6 Test Suite (https://github.com/perl6/roast)
COPYRIGHT AND LICENSE
Copyright 2010, 2011 Nick Patch
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.