【问题标题】:Traversing with a Biapplicative使用 Biapplicative 遍历
【发布时间】:2018-01-12 06:49:00
【问题描述】:

我正在考虑解压缩操作,并意识到表达它们的一种方法是遍历 Biapplicative 函子。

import Data.Biapplicative

class Traversable2 t where
  traverse2 :: Biapplicative p
            => (a -> p b c) -> t a -> p (t b) (t c)

-- Note: sequence2 :: [(a,b)] -> ([a], [b])
sequence2 :: (Traversable2 t, Biapplicative p)
          => t (p b c) -> p (t b) (t c)
sequence2 = traverse2 id

instance Traversable2 [] where
  traverse2 _ [] = bipure [] []
  traverse2 f (x : xs) = bimap (:) (:) (f x) <<*>> traverse2 f xs

在我看来,Traversable 的每个实例都可以机械地转换为 Traversable2 的实例。但是我还没有找到一种使用traverse 来实际实现traverse2 的方法,除了在列表之间进行转换,或者使用unsafeCoerce 进行非常肮脏的把戏。有什么好办法吗?


进一步证明Traversable 是Traversable2:

class (Functor t, Foldable t) => Traversable2 t where
  traverse2 :: Biapplicative p
            => (a -> p b c) -> t a -> p (t b) (t c)
  default traverse2 ::
               (Biapplicative p, Generic1 t, GTraversable2 (Rep1 t))
            => (a -> p b c) -> t a -> p (t b) (t c)
  traverse2 f xs = bimap to1 to1 $ gtraverse2 f (from1 xs)

class GTraversable2 r where
  gtraverse2 :: Biapplicative p
             => (a -> p b c) -> r a -> p (r b) (r c)

instance GTraversable2 V1 where
  gtraverse2 _ x = bipure (case x of) (case x of)

instance GTraversable2 U1 where
  gtraverse2 _ _ = bipure U1 U1

instance GTraversable2 t => GTraversable2 (M1 i c t) where
  gtraverse2 f (M1 t) = bimap M1 M1 $ gtraverse2 f t

instance (GTraversable2 t, GTraversable2 u) => GTraversable2 (t :*: u) where
  gtraverse2 f (t :*: u) = bimap (:*:) (:*:) (gtraverse2 f t) <<*>> gtraverse2 f u

instance (GTraversable2 t, GTraversable2 u) => GTraversable2 (t :+: u) where
  gtraverse2 f (L1 t) = bimap L1 L1 (gtraverse2 f t)
  gtraverse2 f (R1 t) = bimap R1 R1 (gtraverse2 f t)

instance GTraversable2 (K1 i c) where
  gtraverse2 f (K1 x) = bipure (K1 x) (K1 x)

instance (Traversable2 f, GTraversable2 g) => GTraversable2 (f :.: g) where
  gtraverse2 f (Comp1 x) = bimap Comp1 Comp1 $ traverse2 (gtraverse2 f) x

instance Traversable2 t => GTraversable2 (Rec1 t) where
  gtraverse2 f (Rec1 xs) = bimap Rec1 Rec1 $ traverse2 f xs

instance GTraversable2 Par1 where
  gtraverse2 f (Par1 p) = bimap Par1 Par1 (f p)

【问题讨论】:

  • 如果您还没有,请查看 Conor McBride 的“我左边的小丑,右边的小丑:剖析数据结构”。 (cis.upenn.edu/~cis39903/static/clowns.pdf)
  • @Davislor,我认为这与一种完全不同的“解压缩”有关。
  • @Davislor,使用Traversable2 实现Traversable 应该很容易。走另一条路似乎更难。
  • 请注意traverse (f :: a -&gt; f b) ([] :: [a]) = pure ([] :: [b])。这对我来说似乎是一个很好的证据,traverse 不足以实现traverse2,因为因此我们需要一个Applicative 实例,其中pure ([] :: [b]) 称为bipure ([] :: [b]) ([] :: [c]) (arg. 2 (!)) (不知道关于列表的任何特别之处,因为 traverse 的实现应该是提供所有列表特定知识的部分)。 IOW,每次调用pure(和(&lt;*&gt;))都会得到一个单态的东西,而不是一个多态的东西,这会妨碍善意。

标签: haskell traversal bifunctor


【解决方案1】:

我想我可能有适合你的东西。 (编辑:它没有,请参阅 cmets。)您可以在 p () c 和 p b () 上定义新类型并使其成为 Functor 实例。

实施

这又是你的类,带有默认定义。我采用sequenceA 的方式实现sequence2,因为它看起来更简单。

class Functor t => Traversable2 t where
  {-# MINIMAL traverse2 | sequence2 #-}
  traverse2 :: Biapplicative p => (a -> p b c) -> t a -> p (t b) (t c)
  traverse2 f = sequence2 . fmap f

  sequence2 :: Biapplicative p => t (p b c) -> p (t b) (t c)
  sequence2 = traverse2 id

现在,Biapplicative 的“正确部分”是

newtype R p c = R { runR :: p () c }

instance Bifunctor p => Functor (R p) where
  fmap f (R x) = R $ bimap id f x

instance Biapplicative p => Applicative (R p) where
  pure x = R (bipure () x)
  R f <*> R x =
    let f' = biliftA2 const (flip const) (bipure id ()) f
    in  R $ f' <<*>> x

mkR :: Biapplicative p => p b c -> R p c
mkR = R . biliftA2 const (flip const) (bipure () ())

sequenceR :: (Traversable t, Biapplicative p) => t (p b c) -> p () (t c)
sequenceR = runR . sequenceA . fmap mkR

与“左侧部分”大致相同。完整代码在this gist。

现在我们可以将p (t b) () 和p () (t c) 重新组合成p (t b) (t c)。

instance (Functor t, Traversable t) => Traversable2 t where
  sequence2 x = biliftA2 const (flip const) (sequenceL x) (sequenceR x)

我需要为该实例声明打开 FlexibleInstances 和 UndecidableInstances。此外,不知何故 ghc 想要一个 Functor 约束。

测试

我用您的 [] 实例验证了它给出了相同的结果:

main :: IO ()
main = do
  let xs = [(x, ord x - 97) | x <- ['a'..'g']]
  print xs
  print (sequence2 xs)
  print (sequence2' xs)

traverse2' :: Biapplicative p => (a -> p b c) -> [a] -> p [b] [c]
traverse2' _ [] = bipure [] []
traverse2' f (x : xs) = bimap (:) (:) (f x) <<*>> traverse2 f xs

sequence2' :: Biapplicative p => [p b c] -> p [b] [c]
sequence2' = traverse2' id

输出

[('a',0),('b',1),('c',2),('d',3),('e',4),('f',5),('g',6)]
("abcdefg",[0,1,2,3,4,5,6])
("abcdefg",[0,1,2,3,4,5,6])

这是一个有趣的练习!

【讨论】:

  • 我认为这行不通。您遍历序列两次,每个效果执行两次。
  • 你有这方面的例子吗?
  • 使用更刺激的Biapplicative。尝试newtype BIO x y = BIO (IO (x, y))(即Tannen IO (,))并在您的遍历中使用输入和输出。
  • 你说得对,如果我每次都打印一个“+”,我会得到不同数量的加号。所以我认为这个实现违反了Traverse2 的一些法律。
  • 我没有写法律,但我认为Traversable 应该翻译。我认为您的实施将违反组合法。
【解决方案2】:

以下似乎可以解决问题,“仅”利用undefined。可能可遍历的定律保证这是可以的,但我没有试图证明这一点。

{-# LANGUAGE GADTs, KindSignatures, TupleSections #-}

import Data.Biapplicative

import Data.Traversable

data Bimock :: (* -> * -> *) -> * -> * where
   Bimock :: p a b -> Bimock p (a,b)
   Bimfmap :: ((a,b) -> c) -> p a b -> Bimock p c
   Bimpure :: a -> Bimock p a
   Bimapp :: Bimock p ((a,b) -> c) -> p a b -> Bimock p c

instance Functor (Bimock p) where
  fmap f (Bimock p) = Bimfmap f p
  fmap f (Bimfmap g p) = Bimfmap (f . g) p
  fmap f (Bimpure x) = Bimpure (f x)
  fmap f (Bimapp gs xs) = Bimapp (fmap (f .) gs) xs
instance Biapplicative p => Applicative (Bimock p) where
  pure = Bimpure
  Bimpure f<*>xs = fmap f xs
  fs<*>Bimpure x = fmap ($x) fs
  fs<*>Bimock p = Bimapp fs p
  Bimfmap g h<*>Bimfmap i xs = Bimfmap (\(~(a₁,a₂),~(b₁,b₂)) -> g (a₁,b₁) $ i (a₂, b₂))
                              $ bimap (,) (,) h<<*>>xs
  Bimapp g h<*>xs = fmap uncurry g <*> ((,)<$>Bimock h<*>xs)

runBimock :: Biapplicative p => Bimock p (a,b) -> p a b
runBimock (Bimock p) = p
runBimock (Bimfmap f p) = bimap (fst . f . (,undefined)) (snd . f . (undefined,)) p
runBimock (Bimpure (a,b)) = bipure a b
runBimock (Bimapp (Bimpure f) xs) = runBimock . fmap f $ Bimock xs
runBimock (Bimapp (Bimfmap h g) xs)
     = runBimock . fmap (\(~(a₂,a₁),~(b₂,b₁)) -> h (a₂,b₂) (a₁,b₁))
           . Bimock $ bimap (,) (,) g<<*>>xs
runBimock (Bimapp (Bimapp h g) xs)
     = runBimock . (fmap (\θ (~(a₂,a₁),~(b₂,b₁)) -> θ (a₂,b₂) (a₁,b₁)) h<*>)
           . Bimock $ bimap (,) (,) g<<*>>xs

traverse2 :: (Biapplicative p, Traversable t) => (a -> p b c) -> t a -> p (t b) (t c)
traverse2 f s = runBimock . fmap (\bcs->(fmap fst bcs, fmap snd bcs)) $ traverse (Bimock . f) s


sequence2 :: (Traversable t, Biapplicative p)
          => t (p b c) -> p (t b) (t c)
sequence2 = traverse2 id

即使这是安全的,如果它提供了糟糕的性能,我也不会感到惊讶,因为无可辩驳的模式和二次(甚至指数?)元组树构建。

【讨论】:

  • 有什么理由使用undefined 而不是()?
  • @Davislor () 始终具有 () 类型。
  • 好的,所以它在概念上类似于 nnnmmm 的答案,但不是定义两种类型,R 和 L,它们将 a 或 b 替换为 (),或者什么我为我的第一个未发布的解决方案做了,并要求T a 和T b 是Monoids,所以我可以使用mempty,你使用undefined 来解决类型系统。明白了。
  • 我猜它可能可以使用gist.github.com/treeowl/9621f58d55fe0c4f9162be0e074b1b29 中的机制来获得更有原则且(有点)低效的实现。但我不认为这两种方法都有助于无限遍历。
  • 只要你在使用undefined,有没有理由不做这种死的简单的事情,使用toList然后遍历State?
【解决方案3】:

一些观察结果缺乏完整的原始答案。

如果你有一个Biapplicative 双函子,你可以用它做的就是将它应用到某个东西上,然后将它分成与其两个组件同构的一对双函子。

data Helper w a b = Helper {
  left :: w a (),
  right :: w () b
}

runHelper :: forall p a b. Biapplicative p => Helper p a b -> p a b
runHelper x = biliftA2 const (flip const) (left x) (right x)

makeHelper :: (Biapplicative p)
           => p a b -> Helper p a b
makeHelper w = Helper (bimap id (const ()) w)
                      (bimap (const ()) id w)

type Separated w a b = (w a (), w () b)

可以通过将fmap (makeHelper . f) 应用于结构s 来组合@nnnmmm 和@leftroundabout 的方法,从而消除对undefined 的需要,但是您需要使用Helper 或其替换某个类型类的instance,具有可让您解决问题的有用操作。

如果你有一个Traversable 结构,你可以做的是sequenceA Applicative 函子(在这种情况下你的解决方案看起来像traverse2 f = fromHelper . sequenceA . fmap (makeHelper . f),你的Applicative 实例构建一对t结构)或traverse 使用Functor(在这种情况下,您的解决方案将类似于traverse2 f = fromHelper . traverse (g . makeHelper . f) where ...)。无论哪种方式,您都需要定义一个Functor 实例,因为Applicative 继承自Functor。您可以尝试从&lt;&lt;*&gt;&gt; 和bipure id id 或bimap 构建您的Functor,或者您可以在同一通道中处理这两个单独的变量。

不幸的是,要使这些类型适用于 Functor 实例,您必须将 :: p b c 参数化为我们非正式地称为 :: w (b,c) 的类型,其中一个参数是 p 的两个参数的笛卡尔积. Haskell 的类型系统似乎在没有非标准扩展的情况下不允许这样做,但 @leftroundabout 使用 Bimock 类巧妙地解决了这一问题。使用undefined 强制两个分离的函子具有相同的类型。

为了提高性能,您要做的就是不超过一次遍历,这会产生一个与p (t b) (t c) 同构的对象,然后您可以将其转换(类似于自然法则)。因此,您希望实现traverse2 而不是sequence2 并将sequence2 定义为traverse2 id,以避免遍历两次。如果你分离变量并产生与(p (t b) (), p () (t c)) 同构的东西,你可以像@mmmnnn 那样重新组合它们。

在实际使用中,我怀疑您可能希望对问题施加一些额外的结构。您的问题使Bifunctor 的组件b 和c 完全免费,但实际上它们通常是协变或逆变函子,可以用biliftA2 排序或一起遍历Bitraversable 而不是Traversablet,或者甚至可能有 Semigroup、Applicative 或 Monad 实例。

如果您的p 与Monoid 同构,而&lt;&gt; 操作产生的数据结构与您的t 同构,则一个特别有效的优化将是。 (这适用于列表和二叉树;Data.ByteString.Builder 是具有此属性的代数类型。)在这种情况下,操作的关联性让您可以将结构转换为严格的左折叠或惰性右折叠。

这是一个很好的问题,虽然对于一般情况,我没有比 @leftroundabout 更好的代码,但我从中学到了很多东西。

【讨论】:

    【解决方案4】:

    一种稍微邪恶的方法是使用lens 中的Magma 之类的东西。这似乎比 leftaroundabout 的解决方案简单得多,虽然它也不漂亮。

    data Mag a b t where
      Pure :: t -> Mag a b t
      Map :: (x -> t) -> Mag a b x -> Mag a b t
      Ap :: Mag a b (t -> u) -> Mag a b t -> Mag a b u
      One :: a -> Mag a b b
    
    instance Functor (Mag a b) where
      fmap = Map
    
    instance Applicative (Mag a b) where
      pure = Pure
      (<*>) = Ap
    
    traverse2 :: forall t a b c f. (Traversable t, Biapplicative f)
              => (a -> f b c) -> t a -> f (t b) (t c)
    traverse2 f0 xs0 = go m m
      where
        m :: Mag a x (t x)
        m = traverse One xs0
    
        go :: forall x y. Mag a b x -> Mag a c y -> f x y
        go (Pure t) (Pure u) = bipure t u
        go (Map f x) (Map g y) = bimap f g (go x y)
        go (Ap fs xs) (Ap gs ys) = go fs gs <<*>> go xs ys
        go (One x) (One y) = f0 x
        go _ _ = error "Impossible"
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-21
      • 2012-01-21
      • 2016-02-10
      • 2015-10-14
      • 2014-06-10
      相关资源
      最近更新 更多