以课程为例Functor:
class Functor a
instance Functor Maybe
Run Code Online (Sandbox Code Playgroud)
这Maybe是一个类型构造函数.
但我们可以通过其他两种方式实现这一目标:
首先,使用多参数类型:
class MultiFunctor a e
instance MultiFunctor (Maybe a) a
Run Code Online (Sandbox Code Playgroud)
其次使用类型族:
class MonoFunctor a
instance MonoFunctor (Maybe a)
type family Element
type instance Element (Maybe a) a
Run Code Online (Sandbox Code Playgroud)
现在后两种方法有一个明显的优势,即它允许我们做这样的事情:
instance Text Char
Run Code Online (Sandbox Code Playgroud)
要么:
instance Text
type instance Element Text Char
Run Code Online (Sandbox Code Playgroud)
所以我们可以使用单形容器.
第二个优点是我们可以将没有type参数的类型的实例作为最终参数.让我们说我们制作一个Either样式类型但是把类型放错了方法:
data Silly t errorT = Silly t errorT
instance Functor Silly -- oh no we can't do this without a newtype wrapper
Run Code Online (Sandbox Code Playgroud)
而
instance MultiFunctor (Silly t errorT) t
Run Code Online (Sandbox Code Playgroud)
工作得很好
instance MonoFunctor (Silly t errorT)
type instance Element (Silly t errorT) t
Run Code Online (Sandbox Code Playgroud)
也不错.
鉴于在类型类定义中仅使用完整类型(非类型签名)的这些灵活性优势,是否有任何理由使用原始样式定义,假设您使用GHC并且不介意使用扩展?也就是说,你可以做一些特殊的类型构造函数,而不仅仅是类型类中的完整类型,你不能用多参数类型类或类型族来做?
Car*_*arl 12
你的建议忽略了关于现有Functor定义的一些相当重要的细节,因为你没有完成写出类成员函数会发生什么的细节.
class MultiFunctor a e where
mfmap :: (e -> ??) -> a -> ????
instance MultiFunctor (Maybe a) a where
mfmap = ???????
Run Code Online (Sandbox Code Playgroud)
目前的一个重要特性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)
Run Code Online (Sandbox Code Playgroud)
请注意,尝试至少接近可能的推理是多么令人难以置信的复杂.所有功能依赖都适用于允许实例选择而不在整个地方注释类型.(我可能已经错过了几个可能的函数依赖项!)实例本身增加了一些疯狂的类型相等约束,以允许实例选择更可靠.最糟糕的是 - 这仍然比推理具有更差的推理性能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)
Run Code Online (Sandbox Code Playgroud)
当然,这已被打破 - 但它以一种以前甚至无法实现的新方式被打破.定义的一个非常重要的部分Functor是类型a和b中fmap没有出现在实例定义中的任何位置.只看这个类就足以告诉程序员,行为fmap 不能依赖于类型a和b.你可以免费获得这种保证.您不需要相信实例写得正确.
因为fmap免费为您提供保证,所以Functor在定义实例时甚至不需要检查这两个定律.检查法律就足够了fmap id x == x.第一部法律得到证实后,第二部法律免费提供.但是,尽管第二定律不是,但mfmap我刚刚提供的那个破碎mfmap id x == x了.
作为实施者mfmap,您需要做更多工作来证明您的实施是正确的.作为它的用户,你必须更加信任实现的正确性,因为类型系统不能保证这么多.
如果您为其他系统制定了更完整的示例,您会发现如果您想支持其他系统的全部功能,它们会有同样多的问题fmap.这就是他们没有真正使用的原因.它们增加了很多复杂性,只有很小的效用.
嗯,一方面,传统函子类要简单得多。仅此一点就是选择它的充分理由,即使这是 Haskell而不是 Python。它还更好地代表了函子的数学思想:从对象到对象 ( f :: *->*) 的映射,具有额外的属性 ( ->Constraint),即每个 ( forall (a::*) (b::*)) 态射 ( a->b) 都被提升为映射到 ( ) 的相应对象上的态射-> f a->f b)。在类的版本或其等效的 TypeFamilies中,
这些都看不出来。* -> * -> Constraint
从更实际的角度来看,是的,有些事情你只能使用(*->*)->Constraint版本来完成。
特别是,这个约束立即保证所有Haskell 类型都是可以放入函子中的有效对象,而 forMultiFunctor你需要一一检查每个可能包含的类型。有时这是不可能的(或者是吗?),就像当您映射无限多种类型时:
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)
Run Code Online (Sandbox Code Playgroud)
请注意,这不仅使用类型Applicative的实例,还使用其任意元组的实例。我不明白你如何用基于- 或 -的应用类来表达这一点。(如果你制定了合适的 GADT,这可能是可能的,但如果没有的话......可能不会。)faMultiParamTypeClassesTypeFamiliesTough
顺便说一句,这个例子也许并不像看起来那么无用——它基本上表达了单子状态下长度为 2 n的只读向量。