以课程Functor
:
class Functor a
instance Functor Maybe
这里Maybe
是一个类型构造函数。
但我们可以通过其他两种方式做到这一点:
首先,使用多参数类型类:
class MultiFunctor a e
instance MultiFunctor (Maybe a) a
其次使用类型系列:
class MonoFunctor a
instance MonoFunctor (Maybe a)
type family Element
type instance Element (Maybe a) a
现在后两种方法有一个明显的优势,即它允许我们做这样的事情:
instance Text Char
或者:
instance Text
type instance Element Text Char
所以我们可以使用单形容器。
第二个优点是我们可以创建不具有type参数的类型的实例作为最终参数。让我们说我们制作一个Either
样式类型,但是这些类型的方式是错误的:
data Silly t errorT = Silly t errorT
instance Functor Silly -- oh no we can't do this without a newtype wrapper
尽管
instance MultiFunctor (Silly t errorT) t
工作正常,
instance MonoFunctor (Silly t errorT)
type instance Element (Silly t errorT) t
也很好。
鉴于在类型类定义中仅使用完整类型(非类型签名)的这些灵活性优势,是否有任何理由使用原始样式定义,假设您使用GHC并且不介意使用扩展?也就是说,你可以做一些特殊的类型构造函数,而不仅仅是类型类中的完整类型,你可以对多参数类型类或类型族进行处理吗?
答案 0 :(得分:12)
您的提案忽略了有关现有Functor
定义的一些相当重要的细节,因为您没有完成详细说明该类成员函数会发生什么。
class MultiFunctor a e where
mfmap :: (e -> ??) -> a -> ????
instance MultiFunctor (Maybe a) a where
mfmap = ???????
目前fmap
的一个重要属性是它的第一个参数可以改变类型。 fmap show :: (Functor f, Show a) => f a -> f String
。你不能把它扔掉,否则你会失去fmap
的大部分价值。实际上,MultiFunctor
需要看起来更像......
class MultiFunctor s t a b | s -> a, t -> b, s b -> t, t a -> s where
mfmap :: (a -> b) -> s -> t
instance (a ~ c, b ~ d) => MultiFunctor (Maybe a) (Maybe b) c d where
mfmap _ Nothing = Nothing
mfmap f (Just a) = Just (f a)
请注意,尝试推断至少 close 是多么令人难以置信的复杂性。所有功能依赖都适用于允许实例选择而不在整个地方注释类型。 (我可能已经错过了几个可能的函数依赖项!)实例本身增加了一些疯狂的类型相等约束,以允许实例选择更可靠。最糟糕的是 - 这仍然比fmap
具有更差的推理属性。
假设我的前一个实例不存在,我可以写一个这样的实例:
instance MultiFunctor (Maybe Int) (Maybe Int) Int Int where
mfmap _ Nothing = Nothing
mfmap f (Just a) = Just (if f a == a then a else f a * 2)
这当然是破碎的 - 但它以一种以前甚至无法实现的新方式被打破。 <{1}}定义的真正重要部分是Functor
中的a
和b
类型未出现在实例定义中的任何位置。只需查看该类就足以告诉程序员fmap
的行为不能依赖于fmap
和a
类型。你可以免费获得这种保证。您不需要相信实例写得正确。
由于b
免费为您提供保证,因此您甚至无需在定义实例时同时检查fmap
法律。检查法律Functor
就足够了。第一部法律得到证实后,第二部法律免费提供。但是,由于我刚刚提供的fmap id x == x
已被破坏,mfmap
是正确的,即使第二定律不是。
作为mfmap id x == x
的实施者,您需要做更多的工作来证明您的实施是正确的。作为它的用户,你必须更加信任实现的正确性,因为类型系统不能保证这么多。
如果您为其他系统制定了更完整的示例,如果您想支持mfmap
的完整功能,您会发现它们有同样多的问题。这就是他们没有真正使用的原因。他们在实用程序中只增加了一小部分复杂性 lot 。
答案 1 :(得分:4)
嗯,首先,传统的函子类更简单。仅这一点是偏好它的正当理由,即使这是Haskell and not Python。它还更好地代表了仿函数应该是什么的数学概念:从对象到对象的映射(f :: *->*
),以及每个(->Constraint
)的额外属性(forall (a::*) (b::*)
)态射(a->b
)被提升为映射到(-> f a->f b
)的相应对象的态射。在课程的* -> * -> Constraint
版本或其类型的家庭成员中,没有一个可以清楚地看到。
在一个更实用的帐户上,是的,您还可以使用(*->*)->Constraint
版本进行操作。
特别是,这个约束立即保证的是所有 Haskell类型都是可以放入仿函数的有效对象,而对于MultiFunctor
,你需要检查每个可能包含的类型,一个接一个。有时这是不可能的(或者是它?),就像你在无限多种类型上进行映射一样:
data Tough f a = Doable (f a)
| Tough (f (Tough f (a, a)))
instance (Applicative f) = Semigroup (Tough f a) where
Doable x <> Doable y = Tough . Doable $ (,)<$>x<*>y
Tough xs <> Tough ys = Tough $ xs <> ys
-- The following actually violates the semigroup associativity law. Hardly matters here I suppose...
xs <> Doable y = xs <> Tough (Doable $ fmap twice y)
Doable x <> ys = Tough (Doable $ fmap twice x) <> ys
twice x = (x,x)
请注意,这不仅在Applicative
类型上使用f
a
实例MultiParamTypeClasses
,还在其任意元组上使用TypeFamilies
实例。我无法看到您如何使用基于Tough
或{{1}}的应用课程来表达这一点。 (如果你使{{1}}成为一个合适的GADT,可能是可能的,但没有......可能不是。)
答案 2 :(得分:2)
扩展的变体确实更灵活。它被用于例如由Oleg Kiselyov定义restricted monads。粗略地说,你可以拥有
class MN2 m a where
ret2 :: a -> m a
class (MN2 m a, MN2 m b) => MN3 m a b where
bind2 :: m a -> (a -> m b) -> m b
允许monad实例在a
和b
上进行参数化。这很有用,因为您可以将这些类型限制为其他类的成员:
import Data.Set as Set
instance MN2 Set.Set a where
-- does not require Ord
return = Set.singleton
instance Prelude.Ord b => MN3 SMPlus a b where
-- Set.union requires Ord
m >>= f = Set.fold (Set.union . f) Set.empty m
请注意,由于Ord
约束,我们无法使用不受限制的monad定义Monad Set.Set
。实际上,monad类要求monad可用于所有类型。