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

2

13

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.

Orrin answered 1/6, 2021 at 22:22 Comment(10)
Can you migrate from using a tuple to using a sized Vector, which supports zips of arbitrary arity via its Applicative instance (and fixed small arity via the usual zipWith, zipWith3, etc. names)?Instructive
@DanielWagner Thanks for the suggestion, but I would prefer to stick with tuples if possible.Orrin
Can you say a bit about why?Instructive
(1) I would prefer not to have to rewrite existing code that uses tuples, (2) performance/memory concerns: to quote the readme of vector-sized "this approach requires us to carry a runtime representation of length, which is a significant memory overhead for small vectors.". My tuples are small.Orrin
Why do you want a lens based solution? You're not using lenses in the interface, so why do you care how it's implemented? I suspect that anything that builds this functionality out of an ability to generically traverse tuples (like Each) is going to struggle to avoid your problems 2 and 3. But you can really easily make a type class solution, and if your tuples are small then you don't need many instances so the boilerplate isn't too bad.Senescent
Hm. I wonder if that documentation is outdated. You can see in the source that Vectors do not carry a runtime representation of their length (or at least not any extra such representation compared to the efficient underlying vector package's Vectors).Instructive
@Senescent Lenses are not strictly required, I just meant that I would prefer to avoid adding another dependency just to do this.Orrin
@DanielWagner I think the runtime representation of length that they are referring to is the one that the vector package's Vector contains.Orrin
Related (but not optics-based): Is there a zipWith analogue for tuples?Soloist
Seconded on using a sized vector. Tuples are not the right data structure when you need to treat different positions uniformlyTagore
Z
5

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.

Zoellick answered 2/6, 2021 at 3:53 Comment(0)
T
2

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.

Talcahuano answered 4/6, 2021 at 3:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.