使用函子分隔自定义类型值?

Separating custom type values using functors?

我有以下类型:

data Animals a = Cow a a | Dog a a deriving (Show, Eq, Ord)

我试图将不同的函数应用到与动物实例关联的每个 'a' 值。

例如

myCow = "Big" "White"

function1 :: Animals a -> Animals a
function1 cow = --do something with "Big", then something else with "White"

这不适用于我的仿函数定义:

instance Functor Animals where
  fmap f (Cow a b) = Cow (f a) (f b) --I want to apply different functions to 'a' and 'b'! 

如何定义我的仿函数以启用此功能?

您的数据类型 fmap 的类型为 (a -> b) -> Animals a -> Animals b。这非常严格地限制了 fmap 的可能实现,以至于 只有一种可能的实现遵守函子定律(fmap id == idfmap (f . g) == fmap f . fmap g ).

instance Functor Animals where
  fmap f (Cow a b) = ???
  fmap f (Dog a b) = ???

首先,您对函数一无所知 f 除了它可以应用于数据构造函数包装的每个值,所以我们只能用它来做。

其次,我们知道 return 值必须是 Animals b 类型,并且我们不能从 Cow 切换到 Dog,反之亦然(否则,fmap id == id 将不成立)。由此,我们知道实例必须看起来像

instance Functor Animals where
  fmap f (Cow a b) = Cow (f a) (f b)
  fmap f (Dog a b) = Dog (f a) (f b)

类型签名中没有空间来指定第二个函数 g,并且 f 无法知道 Animals 中的哪个 "half"它应用于模拟两个不同功能的值。


如果您改为使用

重新定义您的类型
data Animals' a b = Cow a b | Dog a b
type Animal a = Animals' a a

您可以为 Animals' 定义一个 Bifunctor 的实例,它定义了一个函数 Bimap 可以 对每个函数应用不同的函数一半。

instance Bifunctor Animals' where
  bimap f g (Cow a b) = Cow (f a) (g b)
  bimap f g (Dog a b) = Dog (f a) (g b)

您还获得了两个附加函数 firstsecond,它们将单个函数仅应用于类型构造函数包装的第一种和第二种类型。它们有first f = bimap f idsecond g = bimap id g的默认实现,所以如果你定义了bimap就不需要定义了。 (同样,默认情况下 bimap f g = first f . second g,所以它 足以定义 firstsecond 对而不是 bimap。)

确保您永远不要直接使用 Animals' 以确保您的 none 函数将接受像 Cow 3 "c".

这样的嵌合体

正如 chepner 在他们的回答中恰当地展示的那样,你有一个三难困境需要处理。没有办法让 Animals 具有必须相同类型的字段,同时能够在每个字段上映射不同的函数 享受众所周知且广泛使用的函子 class -- 你必须放弃三个之一:

  • 您可以保留原始类型并编写一个 Functor 实例,对字段执行相同的操作,这是您要避免的...

  • ... 或将 Animals a 更改为 Animals a b,牺牲其中一个不变量以使 Bifunctor 实例成为可能,这是 chepner 的解决方案。 ..

  • ...或者满足于Animals特有的映射函数,它没有通用性但至少实现了你想要的:

animalMap :: (a -> b) -> (a -> b) -> Animals a -> Animals b
animalMap f g ani = case ani of
    Cow x y -> Cow (f x) (g y)
    Dog x y -> Dog (f x) (g y)

权衡利弊,我更倾向于第三种解决方案,尽管它可能令人失望。


这个答案的其余部分是一个长脚注,我将在其中展示一种使第三个解决方案更好一点的方法。我并不是建议您实际使用它——对于您的用例来说,这几乎肯定是矫枉过正——但它是一个很好的可能性,需要注意。

如您所知,Haskell 中函数的一大优点是它们可以组合:

GHCi> ((2+) . (3*) . (4+)) 1
17

组合使我们能够独立于函数所影响的具体数据来思考函数。能够以干净简单的方式进行创作在很多方面都是有益的,可能太多了,无法在此处一一列举。

现在,如果您再次查看 animalMap 并考虑它与 fmap 的相似程度,您将有理由认为每个 函数 a -> b 指定了一种转换(通过 animalMap)一个 Animals 的方法,就像任何单个函数指定了一种转换列表或任何其他 Functor 值的方法(通过fmap):

GHCi> let rex = Dog 2 5
GHCi> let ff = ((2*) . (1+), (3*) . (4+))
GHCi> (\(f, g) -> animalMap f g) ff rex
Dog 6 27
GHCi> -- Or, equivalently:
GHCi> uncurry animalMap ff rex
Dog 6 27
GHCi> -- A different pair:
GHCi> let gg = ((1+), subtract 3)
GHCi> uncurry animalMap gg rex
Dog 3 2

既然如此,那么想要组合要与 animalMap 一起使用的函数对是合理的,就像组合常规函数一样。然而,以显而易见的方式这样做是非常混乱的:

GHCi> uncurry animalMap ((\(h, k) (f, g) -> (h . f, k . g)) gg ff) rex -- yuck
Dog 7 24

当然,您可以通过使用它定义单独的组合函数来避免显式编写丑陋的 lambda,类似于 (.) 但特定于您的用例。不过,这并没有使事情变得那么明朗。

这个故事的转折点在于,实际上有一个标准类型 class 将 (.) 泛化到可以组合的其他事物的功能之外。 class 被称为 Category。如果我们为配对函数定义一个新类型,我们可以给它一个 Category 的实例,如下所示:

import Control.Category
import Prelude hiding (id, (.))

newtype Duof a b = Duof { runDuof :: (a -> b, a -> b) }

instance Category Duof where
    id = Duof (id, id)
    (Duof (h, k)) . (Duof (f, g)) = Duof (h . f, k . g)

接下来,我们不妨用Duof重新定义animalMap

animalMap :: Duof a b -> Animals a -> Animals b
animalMap (Duof (f, g)) ani = case ani of
    Cow x y -> Cow (f x) (g y)
    Dog x y -> Dog (f x) (g y)

最终结果是构图更整洁:

GHCi> let ff = Duof ((2*) . (1+), (3*) . (4+))
GHCi> let gg = Duof ((1+), subtract 3)
GHCi> animalMap (gg . ff) rex
Dog 7 24

请注意,这个新的 animalMap 看起来非常像 Animalsfmap,除了它需要一个 Duof 而不是一个函数。事实上,没有什么可以阻止我们定义一个名为 DuofFunctor 的新类型 class 和方法 duofmap :: Duof a b -> f a -> f b 并使 Animals 成为它的一个实例——没有什么,也就是说,除了定义一个新的通用 class 有点毫无意义,如果你只需要它一个实例。在任何情况下,这个 DuofFunctor 都将完全符合您想要编写 Functor 实例的方式,然后您才意识到这是不可能的。


P.S.: 关于命名约定的评论,与问题本身无关。通常我们会将您的数据类型命名为 Animal,而不是 Animals。即使您的数据类型涵盖多种动物,数据类型的名称几乎总是单数形式,因为这些名称旨在描述该类型的单个值(例如,牛是 Animal,等等是一只狗)。