是否有一个Haskell镜头函数可以“压缩”相同长度的元组?

13

我想要能够使用一个函数将两个同样长度的元组结合起来,类似于base中的zipWith函数。 例如,对于长度为3的元组:

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

虽然我希望有一个适用于任何长度的单一函数。

我已经使用 lens 包制作了一个名为 zipTupleWith 的函数:

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)

只要函数类型为 a -> b -> azipWith可以将两个元组结合在一起。这是因为在参数 a 上使用了 partsOf 函数。

我不满意我的解决方案有三个原因:

  1. 我想使用类型为 a -> b -> c 的函数,这样就可以像 zipTuple = zipTupleWith (,) 这样的东西。
  2. 上面的实现不会捕捉到由于传递不同长度的元组而引起的错误(例如 zipTupleWith (+) (1,2,3) (100,100) = (101, 102, 3) - 我希望这是编译错误)。
  3. 它创建了一个中间列表(b ^.. each)。

那么,有没有一种使用光学器来完成这个任务的方法呢?我看到了 tuple-th 包可以做到这一点,但我更愿意避免为此添加另一个依赖项,并且对于我正在做的事情来说,模板哈斯克尔似乎过度。


1
你能否从使用元组转换为使用sized Vector呢?它通过其Applicative实例支持任意数量的zip(并通过通常的zipWithzipWith3等名称支持固定的小数量)。 - Daniel Wagner
你能稍微说一下为什么吗? - Daniel Wagner
(1) 我宁愿不重写使用元组的现有代码。 (2) 性能/内存问题:引用“vector-sized”的自述文件:“这种方法要求我们携带长度的运行时表示,这对于小向量来说是一个显着的内存开销。”我的元组很小。 - Oli
1
为什么你想要一个基于镜头的解决方案?既然你在接口中没有使用镜头,那么你为什么关心它是如何实现的呢?我怀疑任何通过通用遍历元组(例如 Each)来构建此功能的东西都将难以避免你的问题2和3。但是你可以很容易地制作一个类型类解决方案,如果你的元组很小,那么你不需要太多的实例,所以样板代码并不太糟糕。 - Ben
赞同使用大小固定的向量。当您需要统一处理不同位置时,元组不是正确的数据结构。 - luqui
显示剩余5条评论
2个回答

5

我知道你要求使用基于镜头的方法,但如果你只有少量元组,你可以轻松地通过使用类型类实现你想要的功能。例如,考虑以下内容:

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)

可能有更优雅的编写方式,但模式很直接。您可以轻松添加所需长度元组的实例。


如果你想要任意长度的元组但不想使用模板哈斯克尔,也可以使用泛型方法。以下是一种基于通用表示形状进行zip操作的解决方案:

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)

警告:使用此通用方法进行类型推断效果不佳。


2

看起来你可以做类似这样的事情:

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)

提供:

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

这里的技巧在于“额外”的约束条件Each t s b a。另外两个约束条件由each的两个用法暗示 - 基本上,s ^.. each从s中提取所有a,因此隐含了Each s s a a;而t & unsafePartsOf each %~ ...对于一些... :: [b] -> [c]隐含了Each t v b c。但是添加不必要的约束条件Each t s b a通过断言如果每个bt中都被替换为a,则结果将是s,从而在类型级别上强制实现相等的元组长度。
请注意,这里的并没有做任何神奇的事情。有一堆不同大小的元组的Each实例,类型类中有足够的信息来欺骗它以一种非常丑陋、绕弯路的方式定义zipTupleWith
按照@DDub的回答直接定义所需的类型类会更直接。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接