4
\$\begingroup\$

I am developing a chat system including chat bots and human chat users. Every user must be stored in one large list.

Possible implementation using multiple type constructors:

data User = Human { humanName :: String, humanAge :: Int }
          | Bot { botName :: String, botAge :: Int }
            deriving (Show, Eq)

users :: [User]
users = [Human "Peter" 31, Bot "Your nice chat bot" 12]

To implement the nextMessage function taking the last message and returning a response, I could do this with pattern matching:

type Message = String

nextMessage :: User -> Message -> IO Message
nextMessage (Human _ _) msg = putStr ("New Message: " ++ msg ++ " \n> ") >> getLine
nextMessage (Bot _ _) msg = return $ unwords [msg, "is", "an", "interesting", "question!"]

But this implementation leads to the problem, that pattern matching wont work anymore if I decide to add any fields to the Human or Bot constructors later, so I would have to adjust this every time I change the constructors.

That's why I decided to use the option described here to implement a heterogenous list using type classes.

This is my approach:

{-# LANGUAGE ExistentialQuantification #-}

type Message = String

-- | User wrapper
data SomeUser = forall u. (User u, Show u) => SU u

-- | User type class
class User a where
    -- just getter functions
    getName :: a -> String
    getAge :: a -> Int

    -- function to get the next message, responding to the previous one
    nextMessage :: a -> Message -> IO Message

-- access packed type to implement functions
instance User SomeUser where
    getName (SU u) = getName u
    getAge (SU u) = getAge u
    nextMessage (SU u) = nextMessage u

-- make the wrapper type showable
instance Show SomeUser where
    show (SU u) = show u

-- human chat user
-- gets new messages from user input
data Human = Human { humanName :: String, humanAge :: Int }
             deriving (Show, Eq)

-- automated chat bot
-- calculates new messages
data Bot = Bot { botName :: String, botAge :: Int }
           deriving (Show, Eq)

instance User Human where
    -- implementing the getter functions (annoying)
    getName = humanName
    getAge = humanAge

    -- only function to be implemented differently for different types
    nextMessage human msg = putStr ("New Message: " ++ msg ++ " \n> ") >> getLine

instance User Bot where
    -- implementing the getter functions (annoying)
    getName = botName
    getAge = botAge

    -- only function to be implemented differently for different types
    -- does not need any IO, but has to use it, because of the type declaration
    -- in the type class definition
    nextMessage bot msg = return $ unwords [msg, "is", "an", "interesting", "question!"]

-- list containing every chat user
users :: [SomeUser]
users = [SU (Human "Peter" 31), SU (Bot "Your nice chat bot" 3)]

I like this because it seems to be more generic than the first version, but for every field I need to add a getter, or if needed even a setter, function to the interface to access the values in the same way no matter whether the object is a Human or a Bot.

The second issue, related to both implementations, is the type of the nextMessage function. Because the Human type needs the IO monad to get its answer, every User-like type has to do the calculation inside the IO monad which implies the calculations to be impure although those of the Bot type are perfectly fine, pure code.

Main questions:

  • How to get rid of the annoying getter / (setter) functions in the second implementation?
  • How to handle the need of the IO monad for some types?

See the module at work:

ghci> getName (head users)
"Peter"
ghci> getName (last users)
"Your nice chat bot"
ghci> nextMessage (head users) "Hey, how are you?"
New Message: Hey, how are you? 
> I'm fine!
"I'm fine!"
ghci> nextMessage (last users) "Hey, how are you?"
"Hey, how are you? is an interesting question!"
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Your original problem

that pattern matching wont work anymore if I decide to add any fields to the Human or Bot constructors later, so I would have to adjust this every time I change the constructors.

can be easily solved by matching data constructors without their fields:

nextMessage :: User -> Message -> IO Message
nextMessage Human{} msg = putStr ("New Message: " ++ msg ++ " \n> ") >> getLine
nextMessage Bot{} msg = return $ msg + "is an interesting question!"

In this case changing fields in Human or Bot won't break nextMessage.

You can enable NamedFieldPuns extension to get only specified fields :

{-# LANGUAGE NamedFieldPuns #-}

nextMessage :: User -> Message -> IO Message
nextMessage Human{humanName} msg = putStr ("New Message: " ++ msg ++ " \n> ") >> getLine
nextMessage Bot{botName} msg = return $ msg + " is an interesting question!"

Haskell also allows different datatypes to have common field names, so the code could look like this:

{-# LANGUAGE NamedFieldPuns #-}

data User = Human { name :: String, age :: Int }
          | Bot   { name :: String, age :: Int }
            deriving (Show, Eq)

nextMessage Human{name} msg = putStr ("New Message: " ++ msg ++ " \n> ") >> getLine
nextMessage Bot{name} msg = return $ msg ++ " is an interesting question!"
\$\endgroup\$
3
  • \$\begingroup\$ This is actually quite handy. Do you have any idea for the IO monad problem in the nextMessage function? \$\endgroup\$
    – Erich
    Aug 20, 2017 at 7:56
  • \$\begingroup\$ Without seeing how nextMessage is used, It is not obvious if it worth to split pure code from IO. Mixing pure code and side effects is a bad style, but in your case it is not that much pure code you are mixing. \$\endgroup\$ Aug 20, 2017 at 8:15
  • \$\begingroup\$ Obviously the correct implementation of nextMessage for the Bot type is way more complex than this simple one-liner, so it is definitely worth to get rid of the unnecessary IO monad for the Bot type. \$\endgroup\$
    – Erich
    Aug 20, 2017 at 9:07

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.