在类类中使用类型构造函数的任何优点?

时间:2015-04-06 00:26:12

标签: haskell typeclass

以课程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并且不介意使用扩展?也就是说,你可以做一些特殊的类型构造函数,而不仅仅是类型类中的完整类型,你可以对多参数类型类或类型族进行处理吗?

3 个答案:

答案 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中的ab类型未出现在实例定义中的任何位置。只需查看该类就足以告诉程序员fmap 的行为不能依赖于fmapa类型。你可以免费获得这种保证。您不需要相信实例写得正确。

由于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 n 的只读向量。

答案 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实例在ab上进行参数化。这很有用,因为您可以将这些类型限制为其他类的成员:

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可用于所有类型。

另见:parameterized (indexed) monad