使用类型同义词定义实例

问题描述

抱歉,如果这个问题已经被问过/回答过很多次了——我很难弄清楚问题究竟是什么,因此不知道要搜索什么。


本质上,我定义了一个类:

class (Monadio m) => Logger m where ...

然后我有一个类型(我想说类型同义词,但我不确定这是否是正确的“术语”):

type ResourceOpT r m a = StateT (ResourceCache r) m a

为什么这个实例完全有效:

instance (Monadio m) => Logger ( StateT s m )

但不是这个(我想第一个更抽象/更可取,但我试图理解为什么):

instance (Monadio m) => Logger ( ResourceOpT r m )

根据我对 ResourceOpT 的定义,两者不应该是等价的吗?具体来说,我得到的错误是:

  The type synonym 'ResourceOpT' should have 3 arguments,but has been given 2
  In the instance declaration for 'Logger (ResourceOpT r m)'

我有一种感觉,我正在做的事情在概念上“应该”有效,但要么是我的语法错误,要么是我遗漏了某些东西(可能是语言扩展),或者应该让它起作用。

无论如何,我很想听取您的意见并了解为什么这是错误的,以及为什么我应该/不应该这样做。

提前致谢。

解决方法

错误如下:

类型同义词 ResourceOpT 应该有 3 个参数

类型同义词(用 type 定义;您有正确的术语!)必须应用于与其定义中的参数数量相同数量的类型参数。也就是说,它是类型的“宏”,只是用它的定义代替;它不能像函数那样部分应用。在您的情况下,ResourceOpT 需要三个参数:

type ResourceOpT r m a = StateT (ResourceCache r) m a
              -- ^ ^ ^

此限制使得可以对更高级的类型进行类型推断,即抽象类型构造函数(如 MonadFoldable)的事物。允许部分应用类型同义词意味着编译器无法推导出 (m Int = Either String a) ⇒ (m = Either String,a = Int).

有几个解决方案。一种是从直接解决编译器所说的开始,并更改ResourceOpT定义中的参数数量:

type ResourceOpT r m = StateT (ResourceCache r) m
              --    ^ ---------- no ‘a’ ----------^

然后,输入此代码:

instance (MonadIO m) => Logger ( ResourceOpT s m )

产生不同的消息:

Logger (ResourceOpT s m) 的非法实例声明

(所有实例类型都必须采用 (T t1 ... tn) 形式,其中 T 不是同义词。如果要禁用此功能,请使用 TypeSynonymInstances。)

如果您在源文件中使用 -XTypeSynonymInstances 编译器标志或 {-# LANGUAGE TypeSynonymInstances #-} pragma,它允许为同义词扩展到的类型创建实例。这会产生另一条消息:

Logger (ResourceOpT s m) 的非法实例声明(所有实例类型必须采用 (T a1 ... an) 形式,其中 a1 ... an不同类型变量,每个类型变量出现在在实例头中最多出现一次。如果您想禁用此功能,请使用 FlexibleInstances。)

FlexibleInstances 放宽了您可以创建的实例的一些限制。在使用 monad 转换器编写某些类型的代码时,它经常出现。添加它,此代码被接受。您在这里所做的是为所有 LoggerStateT s ms 创建 m 类的实例,前提是 m 在 {{1 }}。如果有人想为 MonadIO不同 特化创建一个 Logger 实例,而不是 StateT,那么它将被拒绝,或者他们将有跳过一些具有重叠实例的可疑箍。

不需要这些扩展的另一种方法是使用 ResourceCache 而不是类型同义词:

newtype

A newtype ResourceOpT r m a = ResourceOpT { getResourceOpT :: StateT (ResourceCache r) m a } 是一种新类型,而不是同义词。特别是,它是另一种类型的零成本包装器:相同的表示但不同的类型类实例。

这样做,您可以编写或派生 newtypeApplicativeFunctorMonadMonadIO 等的实例,用于具体类型构造函数 MonadState (ResourceCache r),就像 ResourceOpT 中的所有其他转换器一样,如 transformersStateT 等等。您还可以部分应用 ReaderT 构造函数,因为它不是 ResourceOpT 同义词。

一般来说,拥有 type 类的原因是您希望在记录器类型中编写通用代码,因为您有多种不同的类型可以作为实例。但是,特别是如果Logger是唯一的一个,那么你也可以不设在混凝土中类和写入代码ResourceOpT,或多态性ResourceOpT与约束,例如{{1 }}。一般来说,一个函数参数或多态函数比添加一个新的类型类更可取;然而,如果没有类定义和用例的详细信息,很难说是否以及如何重构您的类。

,

Haskell 类型同义词有点像类型级别的宏或缩写。这个想法是,如果你声明一个像

这样的类型同义词
type T a b c = ...

然后无论 T x y z 类型出现在哪里,GHC 都会在内部将其重写为 ...,用 xyz 代替 {{ 1}}、ab

这种替换相当愚蠢和机械,因此 GHC 不允许部分应用类型同义词。也就是说,您不能拥有像 c 这样的类型,因为它不能在没有第三个类型参数的情况下扩展为 T x y。因此,类型同义词必须完全饱和——也就是说,完全应用于参数——无论它们出现在哪里。

与上面 ... 的定义一样,您的 T 类型同义词被声明为接受三个参数,但在您的实例声明中,您仅将其应用于两个。这就是 GHC 抱怨的原因。同样的限制不适用于 ResourceOpT,因为 StateT 不是用 StateT 声明的类型同义词,它本身是一个完全成熟的类型,用 type 声明,所以它不受这样的限制。

有两种方法可以解决这个问题:

  1. 减少您的类型系列接受的类型参数的数量。

    由于 Haskell 的类型系统是高级类型,您可以只用一个参数定义 newtype 类型,如下所示:

    ResourceOpT

    这个定义是等价的,因为 type ResourceOpT r = StateT (ResourceCache r) 仍然会扩展为 ResourceOpT r m aStateT (ResourceCache r) m a 定义的右侧只是部分应用。以这种方式删除参数更普遍地称为 eta 缩减,出于上述原因,在定义类型同义词时通常是一个好主意。

  2. 使用 ResourceOpT 声明代替类型同义词:

    newtype

    这是更多的工作,因为它定义了一个单独的包装器类型而不是类型别名,但是当意图是在新类型上定义新的类型类实例时,通常最好使用类型同义词。

    这样做的原因是类型同义词上的类型类实例将始终与基类型上声明的类型类实例发生冲突。也就是说,在这种情况下,newtype ResourceOpT r m a = ResourceOpT (StateT (ResourceCache r) m a) 将与 instance Logger (ResourceOpT r m) 冲突。那是因为,同样,类型同义词只是缩写,展开后两种类型没有区别,所以两个实例必然重叠。

您决定在这里使用哪个选择取决于您,但我通常建议在涉及类型类实例时使用 instance Logger (StateT s m) 路线。这是更多的工作,但它会为您节省以后的痛苦。如果您确实走这条路,您可能会考虑使用 GHC 的 generalized newtype deriving 功能来减少编写 newtype 时涉及的大部分样板文件。

,

在#haskell IIRC 上询问后,一些好心的人做了一些解释并将我与此联系起来:https://www.haskell.org/onlinereport/haskell2010/haskellch4.html#x10-730004.2.2

基本上,根据我的理解(希望现在是正确的),我的第二个实例示例试图部分应用类型同义词,根据 Haskell2010 标准,这是不合法的。

我最终做的是修改我对 ResourceOpT 的定义(实际上通过省略其他两个术语使其成为部分类型构造函数):

type ResourceOpT r = StateT (ResourceCache r)

然后下面的语句变得合法(因为它是同义词和完整的,而以前不是):

instance (MonadIO m) => Logger (ResourceOpT r m)
,

如前所述,类型 ResourceOpT 具有三个参数 type ResourceOpT r m a。类型构造函数的种类是“类型的类型”。我们可以说ResourceOpT的种类是* -> * -> * -> *

但是当你在实例化它下面使用它时,你只给它两个参数。所以 Haskell 它在抱怨。

换句话说,如果我们应用给定的两个参数,我们有一个类型为 * -> * 的表达式,而 Logger m 接收类型为 * 的东西,因为 Logger 是类型* -> *

简而言之,你必须给它三个参数而不是两个

有关更多信息,您可以查看 Haskell Wiki 的种类 https://wiki.haskell.org/Kind