Haskell 在类型类中使用类型构造函数有什么好处吗?
以类Haskell 在类型类中使用类型构造函数有什么好处吗?,haskell,typeclass,Haskell,Typeclass,以类函子为例: class Functor a instance Functor Maybe 这里的可能是一个类型构造函数 但我们可以通过其他两种方式做到这一点: 首先,使用多参数类型类: class MultiFunctor a e instance MultiFunctor (Maybe a) a 其次,使用类型族: class MonoFunctor a instance MonoFunctor (Maybe a) type family Element type instance
函子为例:
class Functor a
instance Functor 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 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)
或:
所以我们可以使用单态容器
第二个优点是,我们可以创建没有类型参数作为最终参数的类型实例。假设我们制作了一个或样式类型,但将类型放错了方向:
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并且不介意使用扩展,那么是否有理由使用原始样式定义?也就是说,在类型类中放置一个类型构造函数,而不仅仅是一个完整的类型,你能做什么特别的事情吗?这是多参数类型类或类型族所不能做的?首先,传统的函子类要简单得多。这本身就是喜欢它的正当理由,即使这是哈斯克尔。它还更好地代表了函子应该是什么的数学思想:从对象到对象的映射(f:*->*
),具有额外的属性(->约束
),即每个(forall(a:*)(b:*)
)态射(a->b
)被提升到映射到的相应对象上的态射(->fa->fb
)。
在类的*->*->Constraint
版本或其TypeFamilies等价物中,这些都看不清楚
从更实际的角度来看,是的,还有一些事情只能通过(*->*)->约束来完成
特别是,此约束立即向您保证的是,所有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)
data-Tough f a=Doable(f a)
|坚韧的(f(坚韧的f(a,a)))
实例(应用f)=半群(坚韧f a),其中
可行的x可行的y=艰难的。可行的$(,)xy
坚韧的xs坚韧的ys=坚韧的$xs ys
--下面的事实违反了半群结合定律。我想这里没什么关系。。。
xs可行y=xs艰难(可行$fmap两次y)
可行x ys=艰难(可行$fmap两次x)ys
两次x=(x,x)
请注意,这不仅在a
类型上使用了f
的Applicative
实例,而且在其任意元组上也使用了该实例。我看不出如何使用基于MultiParamTypeClasses
或TypeFamilies
的应用类来表达这一点。(如果你让变得强硬
成为一个合适的GADT,这可能是可能的,但如果没有这一点……可能不会。)
顺便说一句,这个例子可能并不像看上去那么无用——它基本上是在一元状态下表示长度为2n的只读向量。扩展的变体确实更灵活。例如,Oleg Kiselyov使用它来定义。大致上,您可以
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
允许通过a
和b
参数化monad实例。这很有用,因为您可以将这些类型限制为其他类的成员:
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在所有类型上都可用
另请参见:.您的建议忽略了有关现有的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)
请注意,要使推断至少接近可能,这是多么复杂。所有函数依赖项都已就位,允许在不注释所有类型的情况下选择实例。(我可能遗漏了其中的几个可能的函数依赖项!)实例本身增加了一些疯狂的类型相等约束,使实例选择更加可靠。最糟糕的是-这仍然比fmap
具有更糟糕的推理属性
假设我的前一个实例不存在,我可以编写这样的实例:
instance Text Char
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)
当然,这是不正确的-但是它以一种以前不可能的新方式被打破了。函子
定义的一个非常重要的部分是fmap
中的a
和b
类型不会出现在实例定义的任何地方。只需查看类就足以告诉程序员fmap
的行为不能依赖于a
和b
的类型。您可以免费获得该保证。您不需要相信实例编写正确
因为fmap
免费为您提供了这种保证,所以在定义实例时,您甚至不需要同时检查Functor
定律。检查定律fmap id x==x
就足够了。当第一定律被证明时,第二定律免费出现。但是由于我刚才提供的mfmap
定律已经失效,mfmap>id x==x
是真的,即使第二定律不是真的
作为mfmap
的实现者,您有更多的工作要做