Reputation: 5459
I want to read a Ratio from a String, but I don't want my program to crash when the denominator is zero. How can I detect a zero denominator and avoid an error? Just using readMaybe
doesn't work:
Prelude Text.Read> readMaybe "1 % 0" :: Maybe Rational
Just *** Exception: Ratio has zero denominator
I created this far from perfect solution:
readMaybeRational :: String -> Maybe Rational
readMaybeRational s =
case ((readMaybe $ drop 1 $ dropWhile (/='%') s) :: Maybe Int)
of Just 0 -> Nothing
_ -> readMaybe s
But I don't know how to handle a nested Ratio nicely:
"Just (1 % 0)"
If I could override Ratio's Read instance, I could get readMaybe to return Nothing when the denominator is zero:
instance (Integral a, Read a) => Read (Ratio a) where
readPrec =
parens
( prec ratioPrec
( do x <- step readPrec
expectP (L.Symbol "%")
y <- step readPrec
-- is y 0? If so, do something here
return (x % y)
)
)
But I'm pretty sure I can't do that.
Upvotes: 10
Views: 251
Reputation: 48612
I think your best solution is a newtype
wrapper around Ratio
, like this:
import Control.Monad
import GHC.Read
import GHC.Real
import qualified Text.Read.Lex as L
import Text.ParserCombinators.ReadPrec
newtype SaneReadRatio a = SaneReadRatio (Ratio a)
type SaneReadRational = SaneReadRatio Integer
instance (Integral a, Read a) => Read (SaneReadRatio a) where
readPrec =
parens
( prec ratioPrec
( do x <- step readPrec
expectP (L.Symbol "%")
y <- step readPrec
guard (y /= 0)
return (SaneReadRatio (x % y))
)
)
readListPrec = readListPrecDefault
readList = readListDefault
Use it by reading in your data with SaneReadRational
in place of Rational
, then using coerce
from Data.Coerce
on the result, which will change it back to the underlying Rational
no matter how deeply it's buried inside your type.
Upvotes: 4