piotrek
piotrek

Reputation: 14520

Operations on tuples for each position individually

i want to do operations on tuples for each position individually. for example

pair_of_sums (a1,b1) (a2,b2) = (a1+a2, b1+b2)

or more general

sum_and_multiplication (a1,b1) (a2,b2) = (a1+a2, b1*b2)

is there any way to make it shorter? like:

pair_of_sums = xxx (+)
sum_and_multiplication = yyy ((+),(*))

Upvotes: 0

Views: 111

Answers (2)

David Young
David Young

Reputation: 10783

One possibility would be to use the bifunctors package which has some useful methods and functions for working with different types including pairs.

pair_of_sums = (<<.>>) . bimap (+) (+)

For pairs, bimap takes two functions and maps the first one over the first element and the second one over the second element (so, for this particular instance, we can view it as having the type bimap :: (a -> a') -> (b -> b') -> (a, b) -> (a', b')). We can see this in its definition

bimap f g ~(a, b) = (f a, g b)

(the ~ essentially just tells Haskell to be a bit more lazy here. It's not really relevant for our purposes.)

In this case, (<<.>>) takes a pair of functions and a pair and it maps each function in the first pair over the corresponding value of second pair. For this use, we can look at it as having he type (<<.>>) :: (a -> a', b -> b') -> (a, b) -> (a', b')).

We can again look at the source code for this function to get an idea of how this works for the (,) instance:

(f, g) <<.>> (a, b) = (f a, g b)

To see how these get put together, we can see that using bimap gives us this (specializing to the (,) instance for Bifunctor, since that is what we are using)

bimap (+) (+) :: Num a => (a, a) -> (a -> a, a -> a)

This gives us a pair of functions that will add the given value to the original value in each position of the pair. For example

λ> fst (bimap (+) (+) (3,5)) 10
13
λ> snd (bimap (+) (+) (3,5)) 100
105

Our composition with (<<.>>) allows us to turn this pair of functions into a function that takes a pair and gives a pair (which fits with the type signature for it I gave above).

Putting these together, we can have a more detailed look at the reduction performed in a call pair_of_sums (3, 5) (10, 100):

pair_of_sums (3, 5) (10, 100)

((<<.>>) . bimap (+) (+))  (3, 5) (10, 100)         -- Definition of pair_of_sums

(\x -> (<<.>>) (bimap (+) (+) x))  (3, 5) (10, 100) -- Definition of (.)

(<<.>>) (bimap (+) (+) (3, 5))                      --
        (10, 100)                                   -- Function application

bimap (+) (+) (3, 5) <<.>> (10, 100)                -- Switch from prefix to infix

((+) 3, (+) 5) <<.>> (10, 100)                      -- After applying bimap for the (,) instance

((+) 3 10, (+) 5 100)                               -- Definition of (<<.>>) for the (,) instance

(13, 105)                                           -- Apply (+)

This is very similar to how the Applicative type class works, but it works with Bifunctor instead of Functor (actually, this is why the type class (<<.>>) belongs to is called Biapply). bimap is analogous to fmap and (<<.>>) is analogous to (<*>).

One cool thing about using this technique to define these functions is that they would work for all of the type constructors that are instances of Biapply, not just (,).

Upvotes: 4

effectfully
effectfully

Reputation: 12715

import Control.Arrow

f = ((uncurry (***) .) .) . (***)

main = do
    print $ f (+) (+) (1, 2) (3, 4) -- (4, 6)
    print $ f (+) (*) (1, 2) (3, 4) -- (4, 8)

Here is how it works:

f                           = ((uncurry (***) .) .) . (***)
f op1 op2                   = uncurry (***) . (op1 *** op2)
f op1 op2  p1       p2      = uncurry (***) ((op1 *** op2) p1) p2
f op1 op2 (x1, y1)  p2      = uncurry (***) ((op1 *** op2) (x1, y1)) p2
f op1 op2 (x1, y1)  p2      = uncurry (***) (op1 x1, op2 y1) p2
f op1 op2 (x1, y1) (x2, y2) = (op1 x1 *** op2 y1) (x2, y2)
f op1 op2 (x1, y1) (x2, y2) = (op1 x1 x2, op2 y1 y2)

Upvotes: 3

Related Questions