自定义Read实例在Max包裹类型时失效,派生实例正常,问题出在哪?
问题描述
我定义了一个表示32位正整数的newtype模块:
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
派生的Show实例表现正常:
λ> positive32 3 Positive32 {getNum = 3} λ> positive32 0 Positive32 {getNum = *** Exception: Attempt to create `Positive32` from `0` CallStack (from HasCallStack): error, called at ...
但直接派生Read实例会允许用户将0包装进Positive32:
λ> read $ "Positive32 {getNum = 0}" :: Positive32 Positive32 {getNum = 0}
因此我自定义了Read实例:
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)
这个实例在基础场景下可以正常工作:
λ> (read $ show $ positive32 3) :: Positive32 Positive32 {getNum = 3}
但当用Max类型包裹Positive32时,解析失败:
λ> import Data.Semigroup (Max(..)) λ> (read $ show $ Max $ positive32 3) :: Max Positive32 Max {getMax = Positive32 {getNum = *** Exception: Prelude.read: no parse
而派生的Read实例却能正常处理该场景:
λ> import Data.Semigroup (Max(..)) λ> (read $ show $ Max $ positive32 3) :: Max Positive32 Max {getMax = Positive32 {getNum = 3}}
请问我的自定义Read实例存在什么问题?
问题分析与修复
你的自定义Read实例主要有两个核心问题:
1. 未处理灵活的空格
派生的Show输出(以及合法的Haskell语法)允许在构造函数、大括号、字段名等位置出现任意数量的空格(包括嵌套场景下的前导空格)。而你的parsePositive32硬编码了固定字符串"Positive32 {getNum = ",完全没有考虑空格的变化——比如当Positive32被Max包裹时,getMax =后面会有一个空格,再紧跟Positive32,但你的解析器会直接尝试匹配开头的Positive32 {getNum = ,忽略了前面的空格,导致匹配失败。
2. 忽略了readsPrec的优先级参数
readsPrec的第一个参数用于指定解析的优先级上下文,当类型被嵌套在其他结构(如Max)中时,解析器需要根据这个参数调整行为(比如是否需要处理括号)。你的实现直接忽略了这个参数,导致在嵌套场景下无法正确适配上下文。
修复方案
使用标准的ReadP组合子处理空格和语法结构,同时正确处理数值合法性:
instance Read Positive32 where readsPrec _ s = readP_to_S parsePositive32 s parsePositive32 :: ReadP Positive32 parsePositive32 = do skipSpaces -- 处理前导空格(包括嵌套场景下的空格) string "Positive32" skipSpaces string "{" skipSpaces string "getNum" skipSpaces string "=" skipSpaces numStr <- munch1 isDigit let num = read numStr :: Word32 skipSpaces string "}" skipSpaces -- 处理后续空格,避免影响外层解析 if num == 0 then pfail -- 拒绝0值,保持类型合法性 else return $ Positive32 num
或者更规范地使用readPrec(推荐,更符合Haskell的Read实例规范):
import Text.Read (Read(..), lexP, Ident, readPrec_to_S, symbol) instance Read Positive32 where readsPrec p = readPrec_to_S readPositive32 p readPositive32 :: ReadPrec Positive32 readPositive32 = do lift $ lexP -- 跳过前导空格并匹配构造函数 Ident "Positive32" <- lexP lift $ symbol "{" Ident "getNum" <- lexP lift $ symbol "=" num <- readPrec -- 复用Word32的Read实例解析数值 lift $ symbol "}" case num of 0 -> pfail _ -> return $ Positive32 num
修复后的实例既能正确拒绝0值,也能处理嵌套在Max等类型中的解析场景。
内容的提问来源于stack exchange,提问作者Enlico
相关产品推荐
相关产品推荐

