(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.
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.