I have a data structure called ClientSession
which I convert to a String
separated by |
for each element and back to the data structure.
18 data ClientSession = ClientSession
19 { sec :: Int
20 , nsec :: Int
21 , username :: String
22 , dbid :: Integer
23 , uuid :: Int
24 , prand :: Int
25 } deriving (Ord, Eq)
26
27 instance Show ClientSession where
28 show (ClientSession
29 { sec = s
30 , nsec = ns
31 , username = un
32 , dbid = db
33 , uuid = ud
34 , prand = pr}) = intercalate "|" ls
35 where ls = [show s, show ns, un, show db, show ud, show pr]
Then I have a set of functions to read
that string and create a ClientSession
from the result. The problem is that this set of functions are so ugly that I lay sleepless at night.
I'm looking for feedback on how to make this code more Haskell. It doesn't feel Haskell to me, it feels like I have solved it in another language and then just ported the code straight off. Like when you translate a natural language in Google Translate.
I have thought of different solutions. Maybe I could use template Haskell, or Typeable/Generics
type classes in some way, or chain the reading using >>=
, or something similar.
I'm still new to Haskell, I've used it on my spare time very little over a couple of years.
37 fromString :: String -> Maybe ClientSession
38 fromString ss = fromParts $ endBy "|" ss
...
74 fromParts :: [String] -> Maybe ClientSession
75 fromParts (s:ns:un:db:ud:pr:[])
76 = newSessionM (readMaybe s) (readMaybe ns) (Just un) (readMaybe db) (readMaybe ud) (readMaybe pr)
77 fromParts _ = Nothing
78
79 newSessionM :: Maybe Int
80 -> Maybe Int
81 -> Maybe String
82 -> Maybe Integer
83 -> Maybe Int
84 -> Maybe Int
85 -> Maybe ClientSession
86 newSessionM (Just s)
87 (Just ns)
88 (Just un)
89 (Just db)
90 (Just ud)
91 (Just pr) = return $ newSession s ns un db ud pr
92 newSessionM _ _ _ _ _ _ = Nothing
93
94 newSession :: Int -> Int -> String -> Integer -> Int -> Int -> ClientSession
95 newSession s ns un db ud pr = ClientSession
96 { sec = s
97 , nsec = ns
98 , username = un
99 , dbid = db
100 , uuid = ud
101 , prand = pr}
1 Answer 1
First note that if your username
contains a bar '|'
, you won't be able to parse the output back. So be sure to check for this.
Since you're already using a parser, it's much easier to read the whole ClientSession
using the parser instead of splitting the string and merging the values manually.
First let's define two helper functions:
import Control.Applicative
import Data.Char (isDigit)
import Data.Functor
import Data.List
import Text.ParserCombinators.ReadP as P
parseInt :: (Read a, Integral a) => ReadP a
parseInt = read <$> munch1 isDigit <* P.optional (char '|')
This one reads one or more digits, consumes '|'
if there is one, and converts the digits into a number. (Combinator <*
runs two actions sequentially, but keeps only the result of the first one.)
parseNotBar :: ReadP String
parseNotBar = munch (/= '|') <* P.optional (char '|')
Similarly tihs second function reads a string until it hits '|'
, consumes the '|'
optionally and returns the string.
Then it's easy to construct a Read
instance. The parser library already has a handy function readP_to_S
that converts a parser into a ReadS
function:
instance Read ClientSession where
readsPrec _ = readP_to_S parseClientSession
where
parseClientSession :: ReadP ClientSession
parseClientSession =
ClientSession <$> parseInt <*> parseInt <*> parseNotBar
<*> parseInt <*> parseInt <*> parseInt
or more shortly
instance Read ClientSession where
readsPrec _ = readP_to_S $
ClientSession <$> parseInt <*> parseInt <*> parseNotBar
<*> parseInt <*> parseInt <*> parseInt
Note: The last part is similar to what you have done in your newSessionM
. Realizing that Maybe
is an Applicative
instance you could have written
newSessionM s ns un db ud pr
= ClientSession <$> s <*> ns <*> un <*> db <*> ud <*> pr
instead. This generalizes it to any Applicative
, not just Maybe
, so it can be used on Maybe
as well as ReadP
or any other applicative parser, for example as
parseClientSession =
newSessionM parseInt parseInt parseNotBar
parseInt parseInt parseInt
However, using <$>
and <*>
makes the notation usually short enough so that we use the combinators directly without the need to define such helper functions.
-
\$\begingroup\$ Now that's the Haskell way to do it! :) Thanks for taking time to explain! I clearly have much to learn. \$\endgroup\$rzetterberg– rzetterberg2013年08月18日 19:30:39 +00:00Commented Aug 18, 2013 at 19:30