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:
a -> b -> c
, allowing things like zipTuple = zipTupleWith (,)
.zipTupleWith (+) (1,2,3) (100,100) = (101, 102, 3)
- I would like this to be a compilation error).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.
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.
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 a
s 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.
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