我有以下类型:
data Animals a = Cow a a | Dog a a deriving (Show, Eq, Ord)
我试图对每个'值与动物实例相关联。
E.g。
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'!
如何定义我的仿函数来启用它?
答案 0 :(得分:3)
您数据类型的fmap
类型为(a -> b) -> Animals a -> Animals b
。这非常严格地限制了fmap
的可能实现,以至于仅只有一个遵循仿函数法则的可能实现(fmap id == id
,fmap (f . g) == fmap f . fmap g
)。 / p>
instance Functor Animals where
fmap f (Cow a b) = ???
fmap f (Dog a b) = ???
首先,您不了解函数f
,除了它可以应用于数据构造函数包装的每个值,因此我们可以用它来做所有事情
其次,我们知道返回值必须是Animals b
类型,并且我们无法从Cow
切换到Dog
,反之亦然(否则,{{ 1}}不会持有)。由此,我们知道实例必须看起来像
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
您可以为data Animals' a b = Cow a b | Dog a b
type Animal a = Animals' a a
定义Bifunctor
的实例,该实例定义了一个函数Animals'
,可以为每一半应用不同的函数。
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)
和first
,它们将单个函数仅应用于由类型构造函数包装的第一个和第二个类型。它们具有second
和first f = bimap f id
的默认实现,因此您无需定义是否定义了second g = bimap id g
。 (同样,默认情况下为bimap
,所以它
足以定义bimap f g = first f . second g
和first
对而不是second
。)
请确保您永远不要直接使用bimap
来确保您的所有功能都不会接受Animals'
之类的chimaera。
答案 1 :(得分:1)
正如chepner在他们的回答中恰当地证明的那样,你有一个处理的三难问题。没有办法让Animals
具有必然相同类型的字段,同时能够在每个字段上映射不同的函数和享受着名和广泛的仿函数类的优点 - 你不得不放下三个中的一个:
您可以保留原始类型并编写一个Functor
实例,它会对字段进行同样的操作,这是您要避免的...
...或将Animals a
更改为Animals a b
,牺牲其中一个不变量,以使Bifunctor
实例成为可能,这是更常见的解决方案。 ..
...或者使用特定于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
的相似程度,那么您可以考虑每个对的函数{{1} }指定一种转换(通过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
当然,您可以通过使用它来定义一个单独的组合函数来避免明确地编写那个丑陋的lambda,类似于GHCi> uncurry animalMap ((\(h, k) (f, g) -> (h . f, k . g)) gg ff) rex -- yuck
Dog 7 24
但特定于您的用例。但是,这并没有让事情变得更加美好。
这个故事中的转折是,实际上有一个标准类型类,它将(.)
超出函数推广到其他可以编写的东西。该类称为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)
重新定义animalMap
:
Duof
最终结果是构图更整洁:
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
fmap
,除了它需要Animals
而不是函数。实际上,没有什么能阻止我们使用方法Duof
定义一个名为DuofFunctor
的新类型类并使duofmap :: Duof a b -> f a -> f b
成为它的一个实例 - 没有任何东西,也就是说,它是如果你只需要一个实例,那么定义一个新的通用类是没有意义的。在任何情况下,这个Animals
都会与您想要编写DuofFunctor
实例的方式完全一致,然后才意识到它是不可能的。
P.S。:关于命名惯例的评论,与问题本身无关。通常,我们会将您的数据类型命名为Functor
,而不是Animal
。尽管您的数据类型涵盖了几种动物,但数据类型几乎总是以单数形式命名,因为这些名称是对该类型的单个值的描述(例如,牛是Animals
,因此是一只狗。)