I am familiar with standard zipWith
functions which operate on corresponding elements of two sequences, but in a functional language (or a language with some functional features), what is the most succinct way to conditionally select the pairs of elements to be zipped, based on a third sequence?
This curiosity arose while scratching out a few things in Excel.
With numbers in A1:A10, B1:B10, C1:C10, D1, E1 and F1, I'm using a formula like this:
{=AVERAGE(IF((D1<=(A1:A10))*((A1:A10)<=E1),B1:B10/C1:C10))}
Each half of the multiplication in the IF statement will produce an array of Boolean values, which are then multiplied (AND'ed) together. Those Booleans control which of the ten quotients will ultimately be averaged, so it's as though ten separate IF statements were being evaluated.
If, for example, only the second and third of the 10 values in A1:A10 satisfy the conditions (both >=D1 and <=E1), then the formula ends up evaluating thusly:
AVERAGE(FALSE,B2/C2,B3/C3,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE)
The AVERAGE function happens to ignore Boolean and text values, so we just get the average of the second and third quotients.
Can this be done as succinctly with Haskell? Erlang? LINQ or F#? Python? etc..
NOTE that for this particular example, the formula given above isn't entirely correct--it was abbreviated to get the basic point across. When none of the ten elements in A1:A10 satisfies the conditions, then ten FALSE values will be passed to AVERAGE, which will incorrectly evaluate to 0.
The formula should be written this way:
{=AVERAGE(IF(NOT(OR((D1<=(A1:A10))*((A1:A10)<=E1))),NA(),
IF((D1<=(A1:A10))*((A1:A10)<=E1),B1:B10/C1:C10)))}
Where the NA()
produces an error, indicating that the average is undefined.
Thanks for the answers. I realized that my first question was pretty trivial, in terms of applying a function on pairs of elements from the second and third lists when the corresponding element from the first list meets some particular criteria. I accepted Norman Ramsey's answer for that.
However, where I went to next was wondering whether the function could be applied to a tuple representing corresponding elements from an arbitrary number of lists--hence my question to Lebertram about the limits of zipWithN
.
Apocalisp's info on applicative functors led me to info on python's unpacking of argument lists--applying a function to an arbitrary number of arguments.
For the specific example I gave above, averaging the quotients of elements of lists (where nums
is the list of lists), it looks like python can do it like this:
from operator import div
def avg(a): return sum(a,0.0)/len(a)
avg([reduce(div,t[1:]) for t in zip(*nums) if d<=t[0] and t[0]<=e])
More generally, with a function f
and a predicate p
(along with avg
) this becomes:
avg([f(t[1:]) for t in zip(*nums) if p(t[0])])
Syntax: zip (*iterables) – the zip () function takes in one or more iterables as arguments. Make an iterator that aggregates elements from each of the iterables.
The result of calling zip () on the iterables is displayed on the right. Notice how the first tuple (at index 0) on the right contains 2 items, at index 0 in L1 and L2, respectively. The second tuple (at index 1) contains the items at index 1 in L1 and L2. In general, the tuple at index i contains items at index i in L1 and L2.
Let's now see how Python's zip () function can help us iterate through multiple lists in parallel. Read ahead to find out. Let's start by looking up the documentation for zip () and parse it in the subsequent sections. Syntax: zip (*iterables) – the zip () function takes in one or more iterables as arguments.
How to conditionally select elements in zip?
Zip first, select later.
In this case, I'm doing the selection with catMaybes
, wihch is often useful in this setting. Getting the to typecheck was a huge pain (must put fromIntegral
in exactly the right spot), but here's the code I would write, relying on the optimizer as usual:
average as bs cs d1 e1 = avg $ catMaybes $ zipWith3 cdiv as bs cs
where cdiv a b c = if a >= d1 && a <= e1 then Just (b/c) else Nothing
avg l = sum l / fromIntegral (length l)
Function cdiv
stands for "conditional division".
To get catMaybes
you have to import Data.Maybe
.
This code typechecks, but I haven't run it.
What you're looking for is Applicative Functors. Specifically the "zippy" applicative from the linked paper.
In Haskell notation, let's call your function f
. Then with applicative programming, it would look as succinct as this:
f d e as bs cs = if' <$> ((&&) <$> (d <=) <*> (e >=))
<$> as <*> ((/) <$> bs <*> cs) <*> (repeat 0)
where if' x y z = if x then y else z
(<*>) = zipWith ($)
The result of f
is a list. Simply take the average. To generify a little:
f g p as bs cs = if' <$> p <$> as <*> (((Some .) . g) <$> bs <*> cs)
<*> (repeat None)
Here, p is a predicate, so you would call it with:
average $ fromMaybe 0 <$> f (/) ((&&) <$> (d <=) <*> (e >=)) as bs cs
... given the same definition of <*>
as above.
Note: I haven't tested this code, so there might be missing parentheses and the like, but this gets the idea across.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With