Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a Haskell lens function for "zipping" same-length tuples?

I would like to be able to combine two tuples of the same length using a function, similar to the zipWith function from base. For example, for the case of length 3 tuples:

zipTupleWith f (a0,a1,a2) (b0,b1,b2) = (f a0 b0, f a1 b1, f a2 b2)

Though I would like a single function that works for any length.

I have made a function zipTupleWith using the lens package:

zipTupleWith :: (Each s1 t1 a a, Each s2 s2 b b) => (a -> b -> a) -> s1 -> s2 -> t1
zipTupleWith f a b = a & partsOf each %~ flip (zipWith f) (b ^.. each)

This can zipWith two tuples so long as the function is of type a -> b -> a. This restriction is because of the partsOf function being used on the argument a.

There are 3 reasons that I'm not happy with my solution:

  1. I would like to be able to use a function of type a -> b -> c, allowing things like zipTuple = zipTupleWith (,).
  2. The above implementation will not catch errors caused by tuples of different lengths being passed in (zipTupleWith (+) (1,2,3) (100,100) = (101, 102, 3) - I would like this to be a compilation error).
  3. It creates an intermediary list (b ^.. each).

So, is there a way of doing this using optics? I saw that the tuple-th package can do this, but I would prefer to avoid adding another dependency just for this, and Template Haskell seems excessive for what I'm doing.

like image 950
Oli Avatar asked Jun 01 '21 22:06

Oli


2 Answers

I know you asked for a lens-based approach, but if you only have small tuples, you can implement what you want with a type class without too much trouble. Consider for instance:

class ZipTuple as a where
  type TupOf as x :: *
  zipTuple :: (a -> b -> c) -> as -> TupOf as b -> TupOf as c

instance ZipTuple (a,a) a where
  type TupOf (a,a) b = (b,b)
  zipTuple f (a1,a2) (b1,b2) = (f a1 b1, f a2 b2)

instance ZipTuple (a,a,a) a where
  type TupOf (a,a,a) b = (b,b,b)
  zipTuple f (a1,a2,a3) (b1,b2,b3) = (f a1 b1, f a2 b2, f a3 b3)

There may be a more elegant way to write this, but the pattern is straightforward. It should be easy to add instances for whatever length tuples you want.


If you want arbitrarily long tuples but don't want template haskell, there's also the Generics route. Here's a solution that zips based on the shape of the generic representation:

import GHC.Generics

class TupleZipG fa fb a b c | fa -> a, fb -> b where
  type Out fa fb a b c :: (* -> *)
  tupleZipG :: (a -> b -> c) -> fa x -> fb x -> Out fa fb a b c x

instance (TupleZipG l1 l2 a b c, TupleZipG r1 r2 a b c) => TupleZipG (l1 :*: r1) (l2 :*: r2) a b c where
  type Out (l1 :*: r1) (l2 :*: r2) a b c = Out l1 l2 a b c :*: Out r1 r2 a b c
  tupleZipG f (l1 :*: r1) (l2 :*: r2) = tupleZipG f l1 l2 :*: tupleZipG f r1 r2

instance TupleZipG (S1 m (Rec0 a)) (S1 m' (Rec0 b)) a b c where
  type Out (S1 m (Rec0 a)) (S1 m' (Rec0 b)) a b c = S1 m (Rec0 c)
  tupleZipG f (M1 (K1 a)) (M1 (K1 b)) = M1 $ K1 $ f a b

instance TupleZipG fa fb a b c => TupleZipG (D1 m fa) (D1 m' fb) a b c where
  type Out (D1 m fa) (D1 m' fb) a b c = D1 m (Out fa fb a b c)
  tupleZipG f (M1 a) (M1 b) = M1 $ tupleZipG f a b

instance TupleZipG fa fb a b c => TupleZipG (C1 m fa) (C1 m' fb) a b c where
  type Out (C1 m fa) (C1 m' fb) a b c = C1 m (Out fa fb a b c)
  tupleZipG f (M1 a) (M1 b) = M1 $ tupleZipG f a b

tupleZip
  :: (TupleZipG (Rep as) (Rep bs) a b c, Generic cs, Generic as,
      Generic bs, Out (Rep as) (Rep bs) a b c ~ Rep cs) =>
     (a -> b -> c) -> as -> bs -> cs
tupleZip f t1 t2 = to $ tupleZipG f (from t1) (from t2)

Warning: Type inference isn't great with this Generic approach.

like image 88
DDub Avatar answered Nov 18 '22 07:11

DDub


It looks like you could do something like this:

zipTupleWith :: (Each s s a a, Each t v b c, Each t s b a)
  => (a -> b -> c) -> s -> t -> v
zipTupleWith f s t = t & unsafePartsOf each %~ zipWith f (s ^.. each)

giving:

> zipTupleWith replicate (1,2,3) ('a','b','c')
("a","bb","ccc")
> zipTupleWith (+) (1,2) (3,4,5)
-- type error

The trick here is the "extra" contraint Each t s b a. The other two contraints are implied by the two uses of each -- basically, Each s s a a is implied by the expression s ^.. each that extracts all the as from s, while Each t v b c is implied by t & unsafePartsOf each %~ ... for some ... :: [b] -> [c]. But adding the otherwise unnecessary constraint Each t s b a enforces equal tuple length at the type level by asserting that IF every b in t was replaced with an a, the result would be an s.

Note that lens isn't doing anything magical here. There's an Each instance for a bunch of different sized tuples, and there's just enough information in the type class to trick it into defining zipTupleWith in a really ugly, roundabout manner.

It would be much more straightforward to just define the type class you need directly, as per @DDub's answer.

like image 2
K. A. Buhr Avatar answered Nov 18 '22 07:11

K. A. Buhr