【问题标题】:What are bifunctors?什么是双函子?
【发布时间】:2017-04-25 17:25:04
【问题描述】:

我对 Haskell 比较陌生,无法理解双函子的实用性。我想我在理论上理解它们:例如,如果我想映射一个抽象多个具体类型的类型,例如 Either 或 Maybe,我需要将它们封装在一个双函子中。但一方面,这些示例似乎特别做作,另一方面,您似乎可以简单地通过组合来实现相同的功能。

例如,我在 Jeremy Gibbons 和 Bruno C. 的 The Essence of the Iterator Pattern 中发现了这段代码。 S.奥利维拉:

import Data.Bifunctor

data Fix s a = In {out::s a (Fix s a) }

map' :: Bifunctor s => (a -> b) -> Fix s a -> Fix s b
map' f = In . bimap f (map' f) . out

fold' :: Bifunctor s => (s a b -> b) -> Fix s a -> b
fold' f = f . bimap id (fold' f) . out

unfold' :: Bifunctor s => (b -> s a b) -> b -> Fix s a
unfold' f = In . bimap id (unfold' f) . f

我理解重点是组合映射和折叠函数来创建迭代器模式,这是通过定义需要两个参数的数据构造函数来实现的。但在实践中,我不明白这与使用常规仿函数并使用 fmap 而不是 bimap 组合函数有何不同。我想我显然必须遗漏一些东西,无论是在这个例子中,还是在一般情况下。

【问题讨论】:

    标签: haskell functor bifunctor


    【解决方案1】:

    我觉得你有点想多了。双函子就像一个双参数函子。 Gibbons 和 Oliveira 的想法只是双函子的一种应用,就像递归方案的标准动物园只是函子的一种应用一样。

    class Bifunctor f where
        bimap :: (a -> c) -> (b -> d) -> f a b -> f c d
    

    Bifunctors 有一种* -> * -> *,两个参数都可以协变映射。将此与常规的 Functors 进行比较,后者只有一个可以协变映射的参数 (f :: * -> *)。

    例如,想想Either 的常用Functor 实例。它只允许您通过第二个类型参数 fmap - Right 值被映射,Left 值保持原样。

    instance Functor (Either a) where
        fmap f (Left x) = Left x
        fmap f (Right y) = Right (f y)
    

    但是,它的 Bifunctor 实例允许您映射总和的两半。

    instance Bifunctor Either where
        bimap f g (Left x) = Left (f x)
        bimap f g (Right y) = Right (g y)
    

    对于元组也是如此:(,)Functor 实例允许您仅映射第二个组件,但 Bifunctor 允许您映射两个部分。

    instance Functor ((,) a) where
        fmap f (x, y) = (x, f y)
    
    instance Bifunctor (,) where
        bimap f g (x, y) = (f x, g y)
    

    请注意,您提到的Maybe 不适合双函子的框架,因为它只有一个参数。


    关于Fix 的问题,双函子的不动点允许您表征具有函子类型参数的递归类型,例如大多数类似容器的结构。让我们以列表为例。

    newtype Fix f = Fix { unFix :: f (Fix f) }
    
    data ListF a r = Nil_ | Cons_ a r deriving Functor
    type List a = Fix (ListF a)
    

    使用标准函子Fix,正如我上面所说,List 没有Functor 实例的通用派生,因为FixLista 一无所知范围。也就是说,我不能写像instance Something f => Functor (Fix f) 这样的东西,因为Fix 的类型不对。我必须手动使用map 来获取列表,也许使用cata

    map :: (a -> b) -> List a -> List b
    map f = cata phi
        where phi Nil_ = Fix Nil_
              phi Cons_ x r = Fix $ Cons_ (f x) r
    

    Fix 的双功能版本确实允许Functor 的实例。 Fix 使用双函子的一个参数来插入 Fix f a 的递归出现,而另一个代表结果数据类型的函子参数。

    newtype Fix f a = Fix { unFix :: f a (Fix f a) }
    
    instance Bifunctor f => Functor (Fix f) where
        fmap f = Fix . bimap f (fmap f) . unFix
    

    所以我们可以这样写:

    deriveBifunctor ''ListF
    
    type List = Fix ListF
    

    并免费获得Functor 实例:

    map :: (a -> b) -> List a -> List b
    map = fmap
    

    当然,如果您想通用地处理具有多个参数的递归结构,那么您需要推广到三函子、四函子等...这显然是不可持续的,并且需要大量工作(在更高级的编程语言)已被用于设计更灵活的系统来表征类型。

    【讨论】:

    • 请问用什么编程语言做什么样的工作?你能指出一些可访问的东西吗?我一直在想有可能定义类似class Covariant (n :: Nat) p 的东西,表示p 在其nth 参数中是协变的,但我不太确定它会是什么样子。
    • 感谢清晰的解释!我认为双函子是多余的语法糖,但是您重载 map 以采用两个参数的示例演示了它们实际上在语义上也更简单。不过,我也对您最后在评论中所指的内容很感兴趣。 OCaml 没有多态函子吗?
    • @dfeuer 哦,我只是参考了用 DT 语言开发的“宇宙”风格的数据类型描述。
    • @dfeuer PS,在这里大声思考,但我认为“Covariant-in-parameter-n”类不是正确的方法。而是制作一个更灵活的Functor,它介于任意类别之间:class (Category c1, Category c2) => Functor c1 c2 f | f -> c1, f -> c2 where fmap :: c1 a b -> c2 (f a) (f b)。那么双函子(在 uncurrying 之后)只是来自产品类别的函子。
    • @dfeuer,这种工作的一个例子(我唯一知道的)是Generic Programming with Indexed Functors。如果你牺牲他们在论文中的一阶性,你会得到something 非常强大,但它仍然不理想,因为你不能例如使索引依赖于参数或相互定义多个数据类型,以使它们具有不同的参数望远镜。
    猜你喜欢
    • 1970-01-01
    • 2016-10-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-12-22
    • 2013-09-13
    • 1970-01-01
    相关资源
    最近更新 更多