使用类型同义词定义实例

Defining instance with a type synonym

如果这已经 asked/answered 多次了,我深表歉意 -- 我很难确定问题到底是什么,因此我真的不知道要搜索什么。


基本上,我有一个 class 我定义了这样的:

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

然后我有一个类型(我想说类型同义词但我不确定这是否正确'term'):

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

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

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

但不是这个(我想第一个更多 abstract/preferrable 但我试图理解为什么):

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)'

我感觉我正在做的事情 'should' 在概念上是可行的,但要么是我的语法错误,要么是我缺少某些东西(可能是语言扩展)或应该使它起作用。

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

提前致谢。

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

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

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

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

更多信息,您可以查看Haskell Wiki for kind https://wiki.haskell.org/Kind

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

type T a b c = ...

那么无论类型 T x y z 出现在哪里,GHC 都会在内部将其重写为 ...,并用 xyz 代替abc

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

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

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

  1. 减少类型族接受的类型参数的数量。

    因为 Haskell 的类型系统更高级,你可以只用一个参数定义你的 ResourceOpT 类型,像这样:

    type ResourceOpT r = StateT (ResourceCache r)
    

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

  2. 使用 newtype 声明而不是类型同义词:

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

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

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

您决定在此处使用哪种选择取决于您,但我通常建议在涉及类型类实例时使用 newtype 路由。这是更多的工作,但它会减轻你以后的痛苦。如果你真的走那条路,你可能会想研究使用 GHC 的 generalized newtype deriving 功能来减少编写 newtypes.

时涉及的大部分样板文件。

在 #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)

错误为:

The type synonym ResourceOpT should have 3 arguments

类型同义词(用 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 )

生成此不同的消息:

Illegal instance declaration for Logger (ResourceOpT s m)

(All instance types must be of the form (T t1 ... tn) where T is not a synonym. Use TypeSynonymInstances if you want to disable this.)

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

Illegal instance declaration for Logger (ResourceOpT s m) (All instance types must be of the form (T a1 ... an) where a1 ... an are distinct type variables, and each type variable appears at most once in the instance head. Use FlexibleInstances if you want to disable this.)

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

不需要这些扩展的一种替代方法是创建 newtype 而不是类型同义词:

newtype ResourceOpT r m a = ResourceOpT
  { getResourceOpT :: StateT (ResourceCache r) m a }

A newtype 是一个新类型,而不是同义词。特别是,它是另一种类型的零成本包装器:相同的表示但类型不同 class 个实例。

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

一般来说,Logger class 的原因是你想在记录器类型中编写通用代码,因为你有多个不同的类型可以是实例。但特别是如果 ResourceOpT 是唯一的,那么您也可以取消 class 并在具体的 ResourceOpT 或具有约束的多态 m 中编写代码作为 MonadState (ResourceCache r) m。一般来说,函数参数或多态函数比添加新类型更可取class;然而,如果没有 class 定义和用例的详细信息,很难说是否以及如何重构你的。