Почему этому новому типу не дают правильный экземпляр Read?

Я создал newtype псевдоним IP введите от Data.IP:

{-# 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. Почему Read экземпляр для IPAddress не идентичен тому, что для IP?

1 ответ

Решение

GHC имеет три механизма для получения экземпляров классов типов:

  • Нормальный механизм получения, описанный в стандарте Haskell, который может извлекать экземпляры для небольшого предварительно определенного набора классов (Eq, Ord, Enum, Bounded, Read, а также Show).
    • Набор классов, которые могут быть получены, может быть расширен с помощью DeriveFunctor, DeriveFoldable, DeriveTraversable, а также DeriveLift расширения, которые при включении обрабатываются так же, как классы, перечисленные в стандарте.
  • GeneralizedNewtypeDeriving, который может получить экземпляры, которые относятся к экземплярам в упакованном типе.
  • DeriveAnyClass, который превращает производные классы в пустые объявления экземпляров.

Здесь есть проблема. Очень легко построить сценарий, в котором для получения экземпляра можно использовать более одного из вышеперечисленных механизмов, и экземпляры могут быть совершенно разными! В вашем примере может применяться как обычное получение, так и получение нового типа. Если вы также включили DeriveAnyClass Это тоже может быть применимо.

Чтобы устранить неоднозначность, GHC использует жестко закодированные правила, которые вы не можете изменить, которые он пытается сверху вниз:

  1. Если класс можно получить с помощью обычного механизма получения, используйте его.
  2. Если DeriveAnyClass включен, используйте это.
  3. Если GeneralizedNewtypeDeriving включен, и объявленный тип данных является новым типом, используйте его.

Обратите внимание, что это означает включение DeriveAnyClass а также GeneralizedNewtypeDeriving в то же время фактически ничего не стоит. Во всяком случае, два нижних правила следует поменять местами, но они не могут быть изменены сейчас.

В вашем случае, так как Read это класс, для которого экземпляр может быть получен с помощью обычного механизма получения, GHC использует его вместо получения нового типа, и вы получаете поведение, которое видите. Это согласуется с тем, как Show тоже работает - выводит Show будет производить экземпляр, который включает в себя IPAddress на выходе - так Read должен следовать тому же формату, чтобы удовлетворить один закон Read есть.

Было бы неплохо, если бы был какой-то механизм, который давал бы указание GHC использовать определенный механизм получения, но в настоящее время его нет. В этом случае вам придется написать экземпляр вручную. К счастью, это не так уж сложно.

Другие вопросы по тегам