Skip to content

Commit 6d5e9dd

Browse files
committed
Use parsers returning an Either as constructors
This change allows the main program to accept a single parser type as input, allowing string arguments to be converted to the correct type or raise an informative error message. This is instead of passing a validating reader and a separate parser.
1 parent 768af8f commit 6d5e9dd

15 files changed

+342
-106
lines changed

app/Bin32.hs

+1-2
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@
1515
--
1616
import Truncate (makeBin32)
1717
import Truncate.Main (btMain)
18-
import Truncate.Main.Readers (binary)
1918

2019
description :: String
2120
description = "Truncate 32-bit floating-point numbers represented in binary"
2221

2322
main :: IO ()
24-
main = btMain binary makeBin32 description
23+
main = btMain makeBin32 description

app/Bin64.hs

+1-2
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@
1515
--
1616
import Truncate (makeBin64)
1717
import Truncate.Main (btMain)
18-
import Truncate.Main.Readers (binary)
1918

2019
description :: String
2120
description = "Truncate 64-bit floating-point numbers represented in binary"
2221

2322
main :: IO ()
24-
main = btMain binary makeBin64 description
23+
main = btMain makeBin64 description

app/Dec32.hs

+1-2
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@
1515
--
1616
import Truncate (makeDec32)
1717
import Truncate.Main (btMain)
18-
import Truncate.Main.Readers (decimal)
1918

2019
description :: String
2120
description = "Truncate 32-bit floating-point numbers represented in decimal"
2221

2322
main :: IO ()
24-
main = btMain decimal makeDec32 description
23+
main = btMain makeDec32 description

app/Dec64.hs

+1-2
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@
1515
--
1616
import Truncate (makeDec64)
1717
import Truncate.Main (btMain)
18-
import Truncate.Main.Readers (decimal)
1918

2019
description :: String
2120
description = "truncate 64-bit floating-point numbers in decimal"
2221

2322
main :: IO ()
24-
main = btMain decimal makeDec64 description
23+
main = btMain makeDec64 description

app/Hex32.hs

+1-2
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@
1515
--
1616
import Truncate (makeHex32)
1717
import Truncate.Main (btMain)
18-
import Truncate.Main.Readers (hexadecimal)
1918

2019
description :: String
2120
description = "Truncate 32-bit floating-point numbers represented in hexadecimal"
2221

2322
main :: IO ()
24-
main = btMain hexadecimal makeHex32 description
23+
main = btMain makeHex32 description

app/Hex64.hs

+1-2
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@
1515
--
1616
import Truncate (makeHex64)
1717
import Truncate.Main (btMain)
18-
import Truncate.Main.Readers (hexadecimal)
1918

2019
description :: String
2120
description = "Truncate 64-bit floating-point numbers represented in hexadecimal"
2221

2322
main :: IO ()
24-
main = btMain hexadecimal makeHex64 description
23+
main = btMain makeHex64 description

fp-truncate.cabal

+3-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ library
2323
hs-source-dirs: src
2424
exposed-modules: Truncate
2525
, Truncate.Main
26-
, Truncate.Main.Readers
2726
, Truncate.Internal
2827
build-depends: base >= 4.7 && < 5
2928
, data-binary-ieee754
@@ -88,6 +87,9 @@ test-suite fp-truncate-test
8887
, Truncate.Test.Truncatable.FromBits
8988
, Truncate.Test.Truncatable.MakeBits
9089
, Truncate.Test.Truncatable.Operator
90+
, Truncate.Test.Binary
91+
, Truncate.Test.Decimal
92+
, Truncate.Test.Hexadecimal
9193
, Truncate.Internal.Test.TruncateWord
9294
, Test.Truncate.Arbitrary
9395
build-depends: base

src/Truncate.hs

+31-16
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ You can create additional representations by making instances of the
2525
--
2626
module Truncate
2727
( Truncatable(..)
28+
, TParser
2829
, WordF(..)
2930
, Binary(..)
3031
, Decimal(..)
@@ -51,6 +52,7 @@ import Data.Binary.IEEE754 ( wordToFloat
5152
import Numeric ( readInt
5253
, readHex
5354
)
55+
import Text.Read (readMaybe)
5456

5557

5658
------------------------------------------------------------------------
@@ -82,6 +84,11 @@ class Truncatable a where
8284
convert = fromBits . makeBits
8385

8486

87+
-----------------------------------------------------------------------
88+
-- | A function type for parsing 'Truncatable' types
89+
type TParser a = String -> Either String a
90+
91+
8592
-----------------------------------------------------------------------
8693
-- | Truncatable floating-point numbers in binary representation.
8794
data Binary = Bin32 String
@@ -108,17 +115,19 @@ toBin = convert
108115

109116
-- | Create a 'Bin32' from a string. The string should be a sequence of the
110117
-- characters @0@ and @1@ no longer than 32 characters in length.
111-
makeBin32 :: String -> Binary
118+
makeBin32 :: TParser Binary
112119
makeBin32 s
113-
| length s <= 32 && validBin s = Bin32 s
114-
| otherwise = error $ "invalid 32-bit binary: " ++ s
120+
| length s > 32 = Left $ tooManyDigits "binary" 32
121+
| (not .validBin) s = Left $ invalidDigits "binary" 32
122+
| otherwise = Right (Bin32 s)
115123

116124
-- | Create a 'Bin64' from a string. The string should be a sequence of the
117125
-- characters @0@ and @1@ no longer than 64 characters in length.
118-
makeBin64 :: String -> Binary
126+
makeBin64 :: TParser Binary
119127
makeBin64 s
120-
| length s <= 64 && validBin s = Bin64 s
121-
| otherwise = error $ "invalid 64-bit binary: " ++ s
128+
| length s > 64 = Left $ tooManyDigits "binary" 64
129+
| (not .validBin) s = Left $ invalidDigits "binary" 64
130+
| otherwise = Right (Bin64 s)
122131

123132

124133
-----------------------------------------------------------------------
@@ -143,13 +152,17 @@ toDec = convert
143152

144153
-- | Create a 'Dec32' from a string. The string can be anything that can be
145154
-- interpreted as a 32-bit 'Float' by 'read'.
146-
makeDec32 :: String -> Decimal
147-
makeDec32 s = Dec32 (read s :: Float)
155+
makeDec32 :: TParser Decimal
156+
makeDec32 s = case readMaybe s :: Maybe Float of
157+
Just f -> Right (Dec32 f)
158+
Nothing -> Left "Error: value is not a 32-bit decimal number"
148159

149160
-- | Create a 'Dec64' from a string. The string can be anything that can be
150161
-- interpreted as a 64-bit 'Double' by 'read'.
151-
makeDec64 :: String -> Decimal
152-
makeDec64 s = Dec64 (read s :: Double)
162+
makeDec64 :: TParser Decimal
163+
makeDec64 s = case readMaybe s :: Maybe Double of
164+
Just f -> Right (Dec64 f)
165+
Nothing -> Left "Error: value is not a 64-bit decimal number"
153166

154167

155168
-----------------------------------------------------------------------
@@ -178,14 +191,16 @@ toHex = convert
178191

179192
-- | Create a 'Hex32' from a string. The string should be a sequence of the
180193
-- valid hexadecimal digits @0-9@ and @a-f@ no longer than 8 characters in length.
181-
makeHex32 :: String -> Hexadecimal
194+
makeHex32 :: TParser Hexadecimal
182195
makeHex32 s
183-
| length s <= 8 && validHex s = Hex32 s
184-
| otherwise = error $ "invalid 32-bit hexadecimal: " ++ s
196+
| length s > 8 = Left $ tooManyDigits "hexadecimal" 32
197+
| (not . validHex) s = Left $ invalidDigits "hexadecimal" 32
198+
| otherwise = Right (Hex32 s)
185199

186200
-- | Create a 'Hex64' from a string. The string should be a sequence of the
187201
-- valid hexadecimal digits @0-9@ and @a-f@ no longer than 16 characters in length.
188-
makeHex64 :: String -> Hexadecimal
202+
makeHex64 :: TParser Hexadecimal
189203
makeHex64 s
190-
| length s <= 16 && validHex s = Hex64 s
191-
| otherwise = error $ "invalid 64-bit hexadecimal: " ++ s
204+
| length s > 16 = Left $ tooManyDigits "hexadecimal" 64
205+
| (not . validHex) s = Left $ invalidDigits "hexadecimal" 64
206+
| otherwise = Right (Hex64 s)

src/Truncate/Internal.hs

+20
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,23 @@ zeroPad n bs
9595

9696
zeroStrip :: String -> String
9797
zeroStrip = dropWhile (== '0')
98+
99+
100+
------------------------------------------------------------------------
101+
-- generating error messages for 'TParser' functions
102+
103+
tooManyDigits :: String -> Int -> String
104+
tooManyDigits name size = errmsg name size "too many digits"
105+
106+
invalidDigits :: String -> Int -> String
107+
invalidDigits name size = errmsg name size "invalid digits"
108+
109+
errmsg :: String -> Int -> String -> String
110+
errmsg name size reason = mconcat [ "Error: value is not a "
111+
, (show size)
112+
, "-bit "
113+
, name
114+
, " number ("
115+
, reason
116+
, ")"
117+
]

src/Truncate/Main.hs

+12-15
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import Options.Applicative
4040

4141
------------------------------------------------------------------------
4242
import Truncate ( Truncatable(..)
43+
, TParser
4344
, toBin
4445
, toDec
4546
, toHex
@@ -52,22 +53,18 @@ import Truncate ( Truncatable(..)
5253
-- and print the output to stdout. This function sets the exit code to @0@
5354
-- on success, and @>0@ otherwise.
5455
btMain :: (Show b, Truncatable b)
55-
=> ReadM String -- ^ A reader for the type of input required by the
56-
-- program for validating (but not parsing) an input value.
57-
-- The 'Tuncate.Main.Readers' module provides readers for
58-
-- binary, decimal and hexadecimal inputs.
59-
-> (String -> b) -- ^ A parser to generate a 'Truncatable' instance from
60-
-- a string (e.g., 'makeBin32').
61-
-> String -- ^ A short (one line) description of program's purpose.
56+
=> TParser b -- ^ A parser to generate a 'Truncatable' instance from
57+
-- a string (e.g., 'makeBin32').
58+
-> String -- ^ A short (one line) description of program's purpose.
6259
-> IO ()
63-
btMain reader parser description =
64-
btMainWithExitCode reader parser description >>= exitWith
60+
btMain parser description =
61+
btMainWithExitCode parser description >>= exitWith
6562

6663
btMainWithExitCode :: (Show b, Truncatable b) =>
67-
ReadM String -> (String -> b) -> String -> IO ExitCode
68-
btMainWithExitCode reader parser description = do
64+
TParser b -> String -> IO ExitCode
65+
btMainWithExitCode parser description = do
6966
prog <- getProgName
70-
let optionParser = makeParser reader parser
67+
let optionParser = makeParser parser
7168
programParser = info (helper <*> optionParser)
7269
(header $ prog ++ " - " ++ description)
7370
options <- execParser programParser
@@ -95,11 +92,11 @@ data Options a = Options { inputValue :: a
9592
}
9693
deriving (Show)
9794

98-
makeParser :: ReadM a -> (a -> b) -> Parser (Options b)
99-
makeParser reader parser = optionParser
95+
makeParser :: TParser b -> Parser (Options b)
96+
makeParser parser = optionParser
10097
where
10198
optionParser = Options <$> valueParser <*> bitsParser <*> baseParser
102-
valueParser = parser <$> (argument reader (metavar "value"))
99+
valueParser = argument (eitherReader parser) (metavar "value")
103100
bitsParser = or12 $ argument auto (metavar "bits")
104101
baseParser = option auto ( short 'b'
105102
<> long "base"

src/Truncate/Main/Readers.hs

-62
This file was deleted.

test/Truncate/Test.hs

+6
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import qualified Truncate.Test.Truncatable.FromBits
2525
import qualified Truncate.Test.Truncatable.MakeBits
2626
import qualified Truncate.Test.Truncatable.Convert
2727
import qualified Truncate.Test.Truncatable.Operator
28+
import qualified Truncate.Test.Binary
29+
import qualified Truncate.Test.Decimal
30+
import qualified Truncate.Test.Hexadecimal
2831
import qualified Truncate.Internal.Test.TruncateWord
2932

3033

@@ -35,5 +38,8 @@ suites = [ Truncate.Test.Truncatable.FromBits.suite
3538
, Truncate.Test.Truncatable.MakeBits.suite
3639
, Truncate.Test.Truncatable.Convert.suite
3740
, Truncate.Test.Truncatable.Operator.suite
41+
, Truncate.Test.Binary.suite
42+
, Truncate.Test.Decimal.suite
43+
, Truncate.Test.Hexadecimal.suite
3844
, Truncate.Internal.Test.TruncateWord.suite
3945
]

0 commit comments

Comments
 (0)