2
\$\begingroup\$

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}
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Aug 18, 2013 at 10:24
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

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.

answered Aug 18, 2013 at 12:02
\$\endgroup\$
1
  • \$\begingroup\$ Now that's the Haskell way to do it! :) Thanks for taking time to explain! I clearly have much to learn. \$\endgroup\$ Commented Aug 18, 2013 at 19:30

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.