Skip to content

Commit 710b6ca

Browse files
committed
Implement precedence-handling bijective read/show
refactors and reimplements Network.Socket.ReadShow to allow for precedence-sensitive Read/Show instances built on an underlying bijective framework. Behavior conforms to derived read/show instances by default but short-circuits to fixed strings when input matches element of a list of paired elements defining a partial bijection. adds several flexible helper-functions to Network.Socket.ReadShow to allow for minimal-boilerplate implementations of bijective read/show for arbitrary types in future. Adds more descriptive documentation for non-obvious properties of Network.Socket.ReadShow types and functions reimplements instance declarations of read/show for types already using bijective read/show definitions Adds cases to Network.SocketSpec test suite to ensure that ReadShow-based Show instances produce expected output for each pattern-bijection type Adds quickcheck-dependent (updated cabal file) tests to ensure roundtrip equality for `read.show` for all types using ReadShow-based instances over arbitrary values, specifically biased towards pattern synonym values
1 parent d4a4bb1 commit 710b6ca

File tree

7 files changed

+324
-151
lines changed

7 files changed

+324
-151
lines changed

Network/Socket/Options.hsc

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -390,8 +390,8 @@ getSockOpt s (SockOpt level opt) = do
390390
peek ptr
391391

392392

393-
socketOptionPairs :: [Pair SocketOption String]
394-
socketOptionPairs =
393+
socketOptionBijection :: Bijection SocketOption String
394+
socketOptionBijection =
395395
[ (UnsupportedSocketOption, "UnsupportedSocketOption")
396396
, (Debug, "Debug")
397397
, (ReuseAddr, "ReuseAddr")
@@ -426,21 +426,19 @@ socketOptionPairs =
426426
, (RecvIPv6PktInfo, "RecvIPv6PktInfo")
427427
]
428428

429-
socketOptionBijection :: Bijection SocketOption String
430-
socketOptionBijection = Bijection{..}
431-
where
432-
cso = "CustomSockOpt"
433-
unCSO = \(CustomSockOpt nm) -> nm
434-
defFwd = defShow cso unCSO _show
435-
defBwd = defRead cso CustomSockOpt _parse
436-
pairs = socketOptionPairs
437-
438429
instance Show SocketOption where
439-
show = forward socketOptionBijection
430+
showsPrec = bijectiveShow socketOptionBijection def
431+
where
432+
defname = "SockOpt"
433+
unwrap = \(CustomSockOpt nm) -> nm
434+
def = defShow defname unwrap showIntInt
440435

441-
instance Read SocketOption where
442-
readPrec = tokenize $ backward socketOptionBijection
443436

437+
instance Read SocketOption where
438+
readPrec = bijectiveRead socketOptionBijection def
439+
where
440+
defname = "SockOpt"
441+
def = defRead defname CustomSockOpt readIntInt
444442

445443
foreign import CALLCONV unsafe "getsockopt"
446444
c_getsockopt :: CInt -> CInt -> CInt -> Ptr a -> Ptr CInt -> IO CInt

Network/Socket/Posix/Cmsg.hsc

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,8 @@ instance Storable IPv6PktInfo where
229229
instance ControlMessage Fd where
230230
controlMessageId = CmsgIdFd
231231

232-
cmsgIdPairs :: [Pair CmsgId String]
233-
cmsgIdPairs =
232+
cmsgIdBijection :: Bijection CmsgId String
233+
cmsgIdBijection =
234234
[ (UnsupportedCmsgId, "UnsupportedCmsgId")
235235
, (CmsgIdIPv4TTL, "CmsgIdIPv4TTL")
236236
, (CmsgIdIPv6HopLimit, "CmsgIdIPv6HopLimit")
@@ -241,17 +241,17 @@ cmsgIdPairs =
241241
, (CmsgIdFd, "CmsgIdFd")
242242
]
243243

244-
cmsgIdBijection :: Bijection CmsgId String
245-
cmsgIdBijection = Bijection{..}
246-
where
244+
instance Show CmsgId where
245+
showsPrec = bijectiveShow cmsgIdBijection def
246+
where
247247
defname = "CmsgId"
248248
unId = \(CmsgId l t) -> (l,t)
249-
defFwd = defShow defname unId _show
250-
defBwd = defRead defname (uncurry CmsgId) _parse
251-
pairs = cmsgIdPairs
252-
253-
instance Show CmsgId where
254-
show = forward cmsgIdBijection
249+
def = defShow defname unId showIntInt
255250

256251
instance Read CmsgId where
257-
readPrec = tokenize $ backward cmsgIdBijection
252+
readPrec = bijectiveRead cmsgIdBijection def
253+
where
254+
defname = "CmsgId"
255+
def = defRead defname (uncurry CmsgId) readIntInt
256+
257+

Network/Socket/ReadShow.hs

Lines changed: 111 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
{-# LANGUAGE RecordWildCards #-}
2+
{-# LANGUAGE LambdaCase #-}
23
{-# LANGUAGE PatternGuards #-}
4+
{-# LANGUAGE ScopedTypeVariables #-}
5+
{-# LANGUAGE RankNTypes #-}
36

47

58
module Network.Socket.ReadShow where
69

10+
import Text.Read ((<++))
711
import qualified Text.Read as P
12+
import qualified Text.Read.Lex as P
13+
import Control.Monad (mzero)
814

915
-- type alias for individual correspondences of a (possibly partial) bijection
1016
type Pair a b = (a, b)
@@ -19,77 +25,112 @@ eqFst x = \(x',_) -> x' == x
1925
eqSnd :: Eq b => b -> (a, b) -> Bool
2026
eqSnd y = \(_,y') -> y' == y
2127

22-
-- | Return RHS element that is paired with provided LHS,
23-
-- or apply a default fallback function if the list is partial
24-
lookForward :: Eq a => (a -> b) -> [Pair a b] -> a -> b
25-
lookForward defFwd ps x
26-
= case filter (eqFst x) ps of
27-
(_,y):_ -> y
28-
[] -> defFwd x
29-
30-
-- | Return LHS element that is paired with provided RHS,
31-
-- or apply a default fallback function if the list is partial
32-
lookBackward :: Eq b => (b -> a) -> [Pair a b] -> b -> a
33-
lookBackward defBwd ps y
34-
= case filter (eqSnd y) ps of
35-
(x,_):_ -> x
36-
[] -> defBwd y
37-
38-
data Bijection a b
39-
= Bijection
40-
{ defFwd :: a -> b
41-
, defBwd :: b -> a
42-
, pairs :: [Pair a b]
43-
}
44-
45-
-- | apply a bijection over an LHS-value
46-
forward :: (Eq a) => Bijection a b -> a -> b
47-
forward Bijection{..} = lookForward defFwd pairs
48-
49-
-- | apply a bijection over an RHS-value
50-
backward :: (Eq b) => Bijection a b -> b -> a
51-
backward Bijection{..} = lookBackward defBwd pairs
52-
53-
-- | show function for Int-like types that encodes negative numbers
54-
-- with leading '_' instead of '-'
55-
_showInt :: (Show a, Num a, Ord a) => a -> String
56-
_showInt n | n < 0 = let ('-':s) = show n in '_':s
57-
| otherwise = show n
58-
59-
-- | parse function for Int-like types that interprets leading '_'
60-
-- as if it were '-' instead
61-
_readInt :: (Read a) => String -> a
62-
_readInt ('_':s) = read $ '-':s
63-
_readInt s = read s
64-
65-
66-
-- | parse a quote-separated pair into a tuple of Int-like values
67-
-- should not be used if either type might have
68-
-- literal quote-characters in the Read pre-image
69-
_parse :: (Read a, Read b) => String -> (a, b)
70-
_parse xy =
71-
let (xs, '\'':ys) = break (=='\'') xy
72-
in (_readInt xs, _readInt ys)
73-
{-# INLINE _parse #-}
74-
75-
-- | inverse function to _parse
76-
-- show a tuple of Int-like values as quote-separated strings
77-
_show :: (Show a, Num a, Ord a, Show b, Num b, Ord b) => (a, b) -> String
78-
_show (x, y) = _showInt x ++ "'" ++ _showInt y
79-
{-# INLINE _show #-}
80-
81-
defShow :: Eq a => String -> (a -> b) -> (b -> String) -> (a -> String)
82-
defShow name unwrap sho = \x -> name ++ (sho . unwrap $ x)
28+
29+
-- | Unified automorphic involution over @Either a b@ that converts between
30+
-- LHS and RHS elements of a list of @Pair a b@ mappings and is the identity
31+
-- function if no matching pair is found
32+
--
33+
-- If list contains duplicate matches, short-circuits to the first matching @Pair@
34+
lookBetween :: (Eq a, Eq b) => [Pair a b] -> Either a b -> Either a b
35+
lookBetween ps = \case
36+
Left x | (_,y):_ <- filter (eqFst x) ps -> Right y
37+
Right y | (x,_):_ <- filter (eqSnd y) ps -> Left x
38+
z -> z
39+
40+
-- Type alias for partial bijections between two types, consisting of a list
41+
-- of individual correspondences that are checked in order and short-circuit
42+
-- on first match
43+
--
44+
-- Depending on how this is used, may not actually be a true bijection over
45+
-- the partial types, as no overlap-checking is currently implemented. If
46+
-- overlaps are unavoidable, the canonical short-circuit pair should appear
47+
-- first to avoid round-trip inconsistencies.
48+
type Bijection a b = [Pair a b]
49+
50+
-- | Helper function for prefixing an optional constructor name before arbitrary values,
51+
-- which only enforces high precedence on subsequent output if the constructor name is not
52+
-- blank and space-separates for non-blank constructor names
53+
namePrefix :: Int -> String -> (Int -> b -> ShowS) -> b -> ShowS
54+
namePrefix i name f x
55+
| null name = f i x
56+
| otherwise = showParen (i > app_prec) $ showString name . showChar ' ' . f (app_prec+1) x
57+
{-# INLINE namePrefix #-}
58+
59+
-- | Helper function for defining bijective Show instances that represents
60+
-- a common use-case where a constructor (or constructor-like pattern) name
61+
-- (optionally) precedes an internal value with a separate show function
62+
defShow :: Eq a => String -> (a -> b) -> (Int -> b -> ShowS) -> (Int -> a -> ShowS)
63+
defShow name unwrap shoPrec = \i x -> namePrefix i name shoPrec (unwrap x)
8364
{-# INLINE defShow #-}
8465

85-
defRead :: Read a => String -> (b -> a) -> (String -> b) -> (String -> a)
86-
defRead name wrap red = \s ->
87-
case splitAt (length name) s of
88-
(x, sn) | x == name -> wrap $ red sn
89-
_ -> error $ "defRead: unable to parse " ++ show s
66+
-- Helper function for stripping an optional constructor-name prefix before parsing
67+
-- an arbitrary value, which only consumes an extra token and increases precedence
68+
-- if the provided name prefix is non-blank
69+
expectPrefix :: String -> P.ReadPrec a -> P.ReadPrec a
70+
expectPrefix name pars
71+
| null name = pars
72+
| otherwise = do
73+
P.lift $ P.expect $ P.Ident name
74+
P.step pars
75+
{-# INLINE expectPrefix #-}
76+
77+
-- | Helper function for defining bijective Read instances that represent a
78+
-- common use case where a constructor (or constructor-like pattern) name
79+
-- (optionally) precedes an internal value with a separate parse function
80+
defRead :: Eq a => String -> (b -> a) -> P.ReadPrec b -> P.ReadPrec a
81+
defRead name wrap redPrec = expectPrefix name $ wrap <$> redPrec
9082
{-# INLINE defRead #-}
9183

92-
-- | Apply a precedence-invariant one-token parse function within ReadPrec monad
93-
tokenize :: (String -> a) -> P.ReadPrec a
94-
tokenize f = P.lexP >>= \(P.Ident x) -> return $ f x
95-
{-# INLINE tokenize #-}
84+
-- | Alias for showsPrec that pairs well with `_readInt`
85+
_showInt :: (Show a) => Int -> a -> ShowS
86+
_showInt = showsPrec
87+
{-# INLINE _showInt #-}
88+
89+
-- | More descriptive alias for `safeInt`
90+
_readInt :: (Bounded a, Integral a) => P.ReadPrec a
91+
_readInt = safeInt
92+
{-# INLINE _readInt #-}
93+
94+
-- | show two elements of a tuple separated by a space character
95+
-- inverse function to readIntInt when used on integer-like values
96+
showIntInt :: (Show a, Show b) => Int -> (a, b) -> ShowS
97+
showIntInt i (x, y) = _showInt i x . showChar ' ' . _showInt i y
98+
{-# INLINE showIntInt #-}
99+
100+
-- | consume and return two integer-like values from two consecutive lexical tokens
101+
readIntInt :: (Bounded a, Integral a, Bounded b, Integral b) => P.ReadPrec (a, b)
102+
readIntInt = do
103+
x <- _readInt
104+
y <- _readInt
105+
return (x, y)
106+
{-# INLINE readIntInt #-}
107+
108+
bijectiveShow :: (Eq a) => Bijection a String -> (Int -> a -> ShowS) -> (Int -> a -> ShowS)
109+
bijectiveShow bi def = \i x ->
110+
case lookBetween bi (Left x) of
111+
Right y -> showString y
112+
_ -> def i x
113+
114+
bijectiveRead :: (Eq a) => Bijection a String -> P.ReadPrec a -> P.ReadPrec a
115+
bijectiveRead bi def = P.parens $ bijective <++ def
116+
where
117+
bijective = do
118+
(P.Ident y) <- P.lexP
119+
case lookBetween bi (Right y) of
120+
Left x -> return x
121+
_ -> mzero
122+
123+
app_prec :: Int
124+
app_prec = 10
125+
{-# INLINE app_prec #-}
126+
127+
-- Parse integral values with type-specific overflow and underflow bounds-checks
128+
safeInt :: forall a. (Bounded a, Integral a) => P.ReadPrec a
129+
safeInt = do
130+
i <- signed
131+
if (i >= fromIntegral (minBound :: a) && i <= fromIntegral (maxBound :: a))
132+
then return $ fromIntegral i
133+
else mzero
134+
where
135+
signed :: P.ReadPrec Integer
136+
signed = P.readPrec

Network/Socket/Types.hsc

Lines changed: 20 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,7 @@ import GHC.IORef (IORef (..))
9191
import GHC.STRef (STRef (..))
9292
import GHC.IO (IO (..))
9393

94-
import Text.Read ((<++))
9594
import qualified Text.Read as P
96-
import qualified Text.Read.Lex as P
9795

9896
#if defined(DOMAIN_SOCKET_SUPPORT)
9997
import Foreign.Marshal.Array
@@ -1316,8 +1314,8 @@ instance Storable In6Addr where
13161314
------------------------------------------------------------------------
13171315
-- Read and Show instance for pattern-based integral newtypes
13181316

1319-
socktypePairs :: [Pair SocketType String]
1320-
socktypePairs =
1317+
socktypeBijection :: Bijection SocketType String
1318+
socktypeBijection =
13211319
[ (UnsupportedSocketType, "UnsupportedSocketType")
13221320
, (Stream, "Stream")
13231321
, (Datagram, "Datagram")
@@ -1327,22 +1325,20 @@ socktypePairs =
13271325
, (NoSocketType, "NoSocketType")
13281326
]
13291327

1330-
socktypeBijection :: Bijection SocketType String
1331-
socktypeBijection = Bijection{..}
1332-
where
1333-
gst = "GeneralSocketType"
1334-
defFwd = defShow gst packSocketType _showInt
1335-
defBwd = defRead gst unpackSocketType _readInt
1336-
pairs = socktypePairs
1337-
13381328
instance Show SocketType where
1339-
show = forward socktypeBijection
1329+
showsPrec = bijectiveShow socktypeBijection def
1330+
where
1331+
gst = "GeneralSocketType"
1332+
def = defShow gst packSocketType _showInt
13401333

13411334
instance Read SocketType where
1342-
readPrec = tokenize $ backward socktypeBijection
1335+
readPrec = bijectiveRead socktypeBijection def
1336+
where
1337+
gst = "GeneralSocketType"
1338+
def = defRead gst unpackSocketType _readInt
13431339

1344-
familyPairs :: [Pair Family String]
1345-
familyPairs =
1340+
familyBijection :: Bijection Family String
1341+
familyBijection =
13461342
[ (UnsupportedFamily, "UnsupportedFamily")
13471343
, (AF_UNSPEC, "AF_UNSPEC")
13481344
, (AF_UNIX, "AF_UNIX")
@@ -1412,19 +1408,17 @@ familyPairs =
14121408
, (AF_CAN, "AF_CAN")
14131409
]
14141410

1415-
familyBijection :: Bijection Family String
1416-
familyBijection = Bijection{..}
1417-
where
1418-
gf = "GeneralFamily"
1419-
defFwd = defShow gf packFamily _showInt
1420-
defBwd = defRead gf unpackFamily _readInt
1421-
pairs = familyPairs
1422-
14231411
instance Show Family where
1424-
show = forward familyBijection
1412+
showsPrec = bijectiveShow familyBijection def
1413+
where
1414+
gf = "GeneralFamily"
1415+
def = defShow gf packFamily _showInt
14251416

14261417
instance Read Family where
1427-
readPrec = tokenize $ backward familyBijection
1418+
readPrec = bijectiveRead familyBijection def
1419+
where
1420+
gf = "GeneralFamily"
1421+
def = defRead gf unpackFamily _readInt
14281422

14291423
-- Print "n" instead of "PortNum n".
14301424
instance Show PortNumber where
@@ -1434,22 +1428,6 @@ instance Show PortNumber where
14341428
instance Read PortNumber where
14351429
readPrec = safeInt
14361430

1437-
app_prec :: Int
1438-
app_prec = 10
1439-
1440-
-- Accept negative values only in parens and check for overflow.
1441-
safeInt :: forall a. (Bounded a, Integral a) => P.ReadPrec a
1442-
safeInt = do
1443-
i <- P.parens unsigned <++ P.parens (P.prec app_prec negative)
1444-
if (i >= fromIntegral (minBound :: a) && i <= fromIntegral (maxBound :: a))
1445-
then return $ fromIntegral i
1446-
else mzero
1447-
where
1448-
unsigned :: P.ReadPrec Integer
1449-
unsigned = P.lift P.readDecP
1450-
negative :: P.ReadPrec Integer
1451-
negative = P.readPrec
1452-
14531431
------------------------------------------------------------------------
14541432
-- Helper functions
14551433

0 commit comments

Comments
 (0)