parsinghaskellfunctional-programmingtypeclass

Custom `Read` instance fails when type is wrapped in Max, but derived instance works - What's wrong with my Read instance?


(Why am I writing Positive32? See my previous question.)

Here's a module exposing a newtype for 32-bits postive integers:

module Positive32 (Positive32, positive32, getNum) where

import Data.Word (Word32)
import Data.Char (isDigit)
import Text.ParserCombinators.ReadP

newtype Positive32 = Positive32 { getNum :: Word32 } deriving (Eq, Ord, Show)

instance Bounded Positive32 where
  minBound = Positive32 1
  maxBound = Positive32 (maxBound :: Word32)

positive32 :: Word32 -> Positive32
positive32 0 = error "Attempt to create `Positive32` from `0`"
positive32 i = Positive32 i

where I've derived Show out of habit, but that's nothing bad:

λ> positive32 3
Positive32 {getNum = 3}
λ> positive32 0
Positive32 {getNum = *** Exception: Attempt to create `Positive32` from `0`
CallStack (from HasCallStack):
  error, called at ...

Then I've realised I also needed a Read instance. However, I can't just derive it, otherwise the client has suddenly a way to wrap 0 into a Positive32:

λ> read $ "Positive32 {getNum = 0}" :: Positive32 
Positive32 {getNum = 0}

So I tried to write a Read instance "myself"¹:

instance Read Positive32 where
  readsPrec _ s = [(positive32 num, rest) | (num, rest) <- readP_to_S parsePositive32 s]

parsePositive32 :: ReadP Word32
parsePositive32 = do
  _ <- string "Positive32 {getNum = "
  num <- munch1 isDigit
  _ <- string "}"
  return (read num)

It seems to work for a very basic case,

λ> (read $ show $ positive32 3) :: Positive32
Positive32 {getNum = 3}

but it fails if I wrap the number in Max

λ> import Data.Semigroup (Max(..))
λ> (read $ show $ Max $ positive32 3) :: Max Positive32
Max {getMax = Positive32 {getNum = *** Exception: Prelude.read: no parse

whereas the derived Read class would handle this case well:

λ> import Data.Semigroup (Max(..))
λ> (read $ show $ Max $ positive32 3) :: Max Positive32
Max {getMax = Positive32 {getNum = 3}}

(¹) Quotes => ChatGPT came up with it; first time it gave me something that compiles, btw.


Solution

  • ChatGPT has here solved (rather awkwardly) the problem of reading representations of the exact form "Positive32 {getNum = 𝑥𝑦𝑧}", but a Read instance needs to be more flexible than that. Ideally it should handle any literal form of Haskell code for the expression; not many instances actually do that but at least they need to handle variable whitespace. This one doesn't allow for any changes at all there, and that's what causes the failure: when reading the string

        "Max {getMax = Positive32 {getNum = 3}}"
    

    your parser is given the control at

        "Max {getMax = Positive32 {getNum = 3}}"
                      ↑
    

    so it sees

                     " Positive32 {getNum = 3}}"
    

    and therefore first needs to discard a space before parsing the constructor. There's a standard way for doing this.

    I would recommend that you make your Read instances also compatible with the non-record style of constructor. For example:

    parsePositive32 = do
       skipSpaces
       string "Positive32"
       skipSpaces
       choice
        [ between (string "{getNum" >> skipSpaces >> string "=")
                  (skipSpaces >> string "}")
                  (skipSpaces >> simpleNum)
        , simpleNum
        ]
     where simpleNum = read <$> munch1 isDigit
    

    Generally, I'd avoid defining Read by hand, as it's tedious and error-prone. Usually it's best to just stick to the derive instance. For Show it can make sense to define a custom instance if the derived one is impractically verbose, but it should still be compatible.