使用函子分隔自定义类型值?
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 == id
,fmap (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)
您还获得了两个附加函数 first
和 second
,它们将单个函数仅应用于类型构造函数包装的第一种和第二种类型。它们有first f = bimap f id
和second g = bimap id g
的默认实现,所以如果你定义了bimap
就不需要定义了。 (同样,默认情况下 bimap f g = first f . second g
,所以它
足以定义 first
和 second
对而不是 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
看起来非常像 Animals
的 fmap
,除了它需要一个 Duof
而不是一个函数。事实上,没有什么可以阻止我们定义一个名为 DuofFunctor
的新类型 class 和方法 duofmap :: Duof a b -> f a -> f b
并使 Animals
成为它的一个实例——没有什么,也就是说,除了定义一个新的通用 class 有点毫无意义,如果你只需要它一个实例。在任何情况下,这个 DuofFunctor
都将完全符合您想要编写 Functor
实例的方式,然后您才意识到这是不可能的。
P.S.: 关于命名约定的评论,与问题本身无关。通常我们会将您的数据类型命名为 Animal
,而不是 Animals
。即使您的数据类型涵盖多种动物,数据类型的名称几乎总是单数形式,因为这些名称旨在描述该类型的单个值(例如,牛是 Animal
,等等是一只狗)。
我有以下类型:
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 == id
,fmap (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)
您还获得了两个附加函数 first
和 second
,它们将单个函数仅应用于类型构造函数包装的第一种和第二种类型。它们有first f = bimap f id
和second g = bimap id g
的默认实现,所以如果你定义了bimap
就不需要定义了。 (同样,默认情况下 bimap f g = first f . second g
,所以它
足以定义 first
和 second
对而不是 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
看起来非常像 Animals
的 fmap
,除了它需要一个 Duof
而不是一个函数。事实上,没有什么可以阻止我们定义一个名为 DuofFunctor
的新类型 class 和方法 duofmap :: Duof a b -> f a -> f b
并使 Animals
成为它的一个实例——没有什么,也就是说,除了定义一个新的通用 class 有点毫无意义,如果你只需要它一个实例。在任何情况下,这个 DuofFunctor
都将完全符合您想要编写 Functor
实例的方式,然后您才意识到这是不可能的。
P.S.: 关于命名约定的评论,与问题本身无关。通常我们会将您的数据类型命名为 Animal
,而不是 Animals
。即使您的数据类型涵盖多种动物,数据类型的名称几乎总是单数形式,因为这些名称旨在描述该类型的单个值(例如,牛是 Animal
,等等是一只狗)。