是否有一个用于元组的zipWith模拟?

dup*_*ode 8 haskell tuples applicative

初步说明:这是SeanD 删除的问题的重新定位.

就像zipWith列表一样......

GHCi> zipWith (+) [1,2] [3,4]
[4,6]
Run Code Online (Sandbox Code Playgroud)

......感觉就像元素一样,应该有类似的东西......

tupleZipWith (+) (1,2) (3,4)
Run Code Online (Sandbox Code Playgroud)

......但是在基地似乎没有任何明显的东西.我有哪些选择?

dup*_*ode 10

一个选项是使用tuples-homogenous-h98包,它为具有适当Applicative实例的同类元组提供newtype包装:

GHCi> import Data.Tuple.Homogenous
GHCi> import Control.Applicative
GHCi> liftA2 (+) (Tuple2 (1,2)) (Tuple2 (3,4))
Tuple2 {untuple2 = (4,6)}
GHCi> (>) <$> Tuple3 (7,4,7) <*> Tuple3 (6,6,6)
Tuple3 {untuple3 = (True,False,True)}
Run Code Online (Sandbox Code Playgroud)

如果你有一个最喜欢的同源元组/固定大小的矢量/固定大小的列表库而不是tuples-homogenous-h98,那么它也会有ZipList类似合适的Applicative实例.


对于成对的问题略有不同,您可能需要考虑Data.Biapplicative来自bifunctors:

GHCi> import Data.Biapplicative
GHCi> bimap (+) (+) (1,2) <<*>> (3,4)
(4,6)
Run Code Online (Sandbox Code Playgroud)

这种方法的一个好处是它可以处理异构对:

GHCi> bimap (+) (+) (1,2.5) <<*>> (3,4)
(4,6.5)
GHCi> bimap (+) (++) (1,"foo") <<*>> (3,"bar")
(4,"foobar")
Run Code Online (Sandbox Code Playgroud)


Li-*_*Xia 5

使用GHC Generics,我们可以定义仅依赖于类型结构(构造函数及其arities)的操作.

我们想要一个函数zipWithP,它接受一个函数,ff在匹配的字段之间应用两个元组.或许带有符合此标志的东西:

zipWithP
  :: forall c s. _
  => (forall s. c s => s -> s -> s) -> a -> a -> a
Run Code Online (Sandbox Code Playgroud)

f :: forall s. c s => s -> s -> s是多态的,允许元组是异构的,只要字段是所有实例c.只要有效,_约束就会捕获该要求,这取决于实现.

有些库可以捕获常见的构造,特别是单行泛型.

随着自动化程度的提高......


经典的解决方案是使用该GHC.Generics模块.甲Generic实例表示用户定义类型之间的同构a和一个"一般表示" Rep a与它相关联.

此通用表示由一组固定的类型构成GHC.Generics.(该模块的文档包含有关该表示的更多详细信息.)

标准步骤是:

  1. 在固定的一组类型上定义函数(可能是它的一个子集);

  2. 通过使用Generic实例给出的同构,使它们适应用户定义的类型.

步骤1通常是类型类.以下GZipWith是可以压缩的通用表示类.这里处理的类型构造函数按重要性降序排列:

  • K1代表字段(只是申请f);
  • (:*:) 代表类型产品(分别压缩操作数);
  • M1NEWTYPE携带在类型级别的信息,我们使用的不是这里,所以我们只是包装/用它打开;
  • U1 代表无效的构造函数,主要是为了完整性.

步骤2 zipWithP通过gZipWithfrom/ to在适当的地方合成来定义.

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}

import GHC.Generics

class GZipWith c f where
  gZipWith :: (forall s. c s => s -> s -> s) -> f p -> f p -> f p

instance c a => GZipWith c (K1 _i a) where
  gZipWith f (K1 a) (K1 b) = K1 (f a b)

instance (GZipWith c f, GZipWith c g) => GZipWith c (f :*: g) where
  gZipWith f (a1 :*: a2) (b1 :*: b2) = gZipWith @c f a1 b1 :*: gZipWith @c f a2 b2

instance GZipWith c f => GZipWith c (M1 _i _c f) where
  gZipWith f (M1 a) (M1 b) = M1 (gZipWith @c f a b)

instance GZipWith c U1 where
  gZipWith _ _ _ = U1

zipWithP
  :: forall c a. (Generic a, GZipWith c (Rep a))
  => (forall s. c s => s -> s -> s) -> a -> a -> a
zipWithP f a b = to (gZipWith @c f (from a) (from b))

main = do
  print (zipWithP @Num (+) (1,2) (3,4) :: (Int, Integer))
Run Code Online (Sandbox Code Playgroud)

generics-sop提供高级组合器,通常可以使用fmap/traverse/zip...

在这种情况下,相关的组合器是hcliftA2用二元函数来压缩通用的异构元组元组.代码后有更多解释.

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}

import Control.Applicative (liftA2)
import Data.Proxy (Proxy(..))
import Generics.SOP

zipWithP
  :: forall c a k
  .  (Generic a, Code a ~ '[k], All c k)
  => (forall s. c s => s -> s -> s) -> a -> a -> a
zipWithP f x y =
  case (from x, from y) of
    (SOP (Z u), SOP (Z v)) ->
      to (SOP (Z (hcliftA2 (Proxy :: Proxy c) (liftA2 f) u v)))

main = do
  print (zipWithP @Num (+) (1,2) (3,4) :: (Int, Integer))
Run Code Online (Sandbox Code Playgroud)

从顶部开始zipWithP.

约束:

  • Code a ~ '[k]:a必须是单构造函数类型(Code a :: [[*]]是构造函数的列表a,每个构造函数都作为其字段列表给出).
  • All c k:构造函数的所有字段都k满足约束c.

身体:

  • from从常规类型映射a到通用的产品总和(SOP I (Code a)).
  • 我们假设该类型a有一个构造函数.我们通过模式匹配来应用这些知识来摆脱"总和"层.我们得到uv,其类型产品(NP I k).
  • 我们申请hcliftA2拉链两个元组uv.
  • 字段包含在类型构造函数I/ Identity(函子函子HKD样式)中,因此liftA2顶部还有一个图层f.
  • 我们得到一个新的元组,并通过应用构造函数和to(反过来)从前两个步骤向后移动from.

有关更多详细信息,请参阅generics-sop文档.


zipWithP属于一类通常由"为每个字段执行此操作"描述的操作.单行导出操作,其中一些名称可能看起来很熟悉(map...,traverse...),它们本质上是与任何泛型类型相关联的单个"广义遍历"的特化.

特别zipWithP是被称为binaryOp.

{-# LANGUAGE TypeApplications #-}

import Generics.OneLiner

main = print (binaryOp @Num (+) (1,2) (3,4) :: (Int, Integer))
Run Code Online (Sandbox Code Playgroud)