为什么这个新类型没有被赋予正确的 Read 实例?

Why isn’t this newtype being given the right Read instance?

我从 Data.IP:

创建了 IP 类型的 newtype 别名
{-# LANGUAGE GeneralizedNewtypeDeriving #-}

module IPAddress (IPAddress) where

import Data.IP (IP)
import Database.PostgreSQL.Simple.ToField

newtype IPAddress = IPAddress IP
    deriving (Read, Show)

instance ToField IPAddress where
    toField ip = toField $ show ip

(我想让它成为 ToField 的实例而不创建孤立实例。)

不过,新类型似乎没有以应有的方式支持 Read。在此 GHCi 转录本中,您可以看到给定的字符串可以解释为 IP 但不能解释为 IPAddress:

*Main IPAddress> :m + Data.IP
*Main IPAddress Data.IP> read "1.2.3.4" :: IP
1.2.3.4
*Main IPAddress Data.IP> read "1.2.3.4" :: IPAddress
IPAddress *** Exception: Prelude.read: no parse

无论我是否启用 GeneralizedNewtypeDeriving,行为都是相同的。为什么 IPAddressRead 实例与 IP 的实例不同?

GHC 具有三种派生类型class 实例的机制:

  • Haskell 标准中概述的 normal deriving mechanism,它可以派生一小部分预定义的 class 实例(EqOrdEnumBoundedReadShow)。
    • 可以使用 DeriveFunctorDeriveFoldableDeriveTraversableDeriveLift 扩展来扩展可以导出的 classes 集,这启用后,与标准中列出的 classes 的处理方式相同。
  • GeneralizedNewtypeDeriving,它可以派生遵循包装类型实例的实例。
  • DeriveAnyClass,将派生的 classes 转换为空实例声明。

这里有问题。构造一个场景非常容易,其中可以使用上述机制中的一种以上来派生实例,并且实例可以完全不同!在您的示例中,普通派生 新类型派生都可以适用。如果您还启用了 DeriveAnyClass,它也适用。

为了消除歧义,GHC 使用您无法更改的硬编码规则,它从上到下尝试:

  1. 如果 class 可以使用普通派生机制派生,请使用它。
  2. 如果启用了 DeriveAnyClass,请使用它。
  3. 如果启用 GeneralizedNewtypeDeriving 并且声明的数据类型是新类型,则使用它。

请注意,这意味着同时打开 DeriveAnyClassGeneralizedNewtypeDeriving 实际上毫无价值。如果有的话,最下面的两条规则应该交换一下,但现在真的不能改变了。

在你的例子中,因为 Read 是一个 class 可以通过普通的派生机制派生出一个实例,GHC 使用那个而不是使用新类型派生,你得到了行为你看。这也与 Show 的工作方式一致——派生 Show 将产生一个在输出中包含 IPAddress 的实例——因此 Read 应该遵循相同的格式以满足一法Read有。

如果有某种机制可以指示 GHC 使用特定的派生机制就好了,但目前还没有。在这种情况下,您必须手动编写实例。幸运的是,它并不难。