patternMinor
Very simple in-memory database
Viewed 0 times
databasesimplememoryvery
Problem
I've implemented a very simple in-memory "database" just as an exercise. I wanted to see if there's anything obvious I should be doing that fits more of the Haskell way of doing things.
import qualified Data.Map as M
type History = [String]
data Result = Result (Maybe String) Bool DB
data Command = Command { name :: String
, key :: Maybe String
, value :: Maybe String
}
data DB = DB (M.Map String String)
cmdKey c = let Just s = key c in s
cmdValue c = let Just s = value c in s
runSetCmd :: Command -> DB -> IO (Maybe String, DB)
runSetCmd c db@(DB map) = do
let newMap = M.insert (cmdKey c) (cmdValue c) map
return $ (Nothing, DB newMap)
runGetCmd :: Command -> DB -> IO (Maybe String, DB)
runGetCmd c db@(DB map) = return $ (M.lookup (cmdKey c) map, db)
execCmd :: DB -> Command -> IO Result
execCmd db c@(Command name key value) = do
(output,newDB) runSetCmd c db
"get" -> runGetCmd c db
_ -> return (Nothing, db)
return $ Result output end newDB
where
end = case name of
"end" -> True
_ -> False
getCmd = getLine >>= return . parseCmd
parseCmd :: String -> Command
parseCmd s =
case words s of
(name:key:value:_) -> Command name (Just key) (Just value)
(name:key:[]) -> Command name (Just key) Nothing
(name:[]) -> Command name Nothing Nothing
displayResult :: Result -> IO Result
displayResult r@(Result (Just s) _ _) = putStrLn s >> return r
displayResult r = return r
continue :: Result -> IO ()
continue (Result _ end db) = if end then return () else repl db
repl state = getCmd >>= execCmd state >>= displayResult >>= continue
main = repl (DB M.empty)Solution
First of all, as Karl said, you should minimize the code in IO monad and make sure most of your code is in pure functions
Second your Command type is not properly designed
Having the name as String is not cool. You could have a type something like
...but still your command type has too many invalid combinations which the type system cannot help you with (for example: name="get", key="xyz", value="asdfasdf"). Your type should be designed such that you avoid these combinations and you make the type system work for you.
Given this I would define the Command type like this:
if your type is like this then execCmd will become:
If you only address these two issues you will see a lot of simplification in your code. Other things would be:
Using the State monad will help you with the passing of the current db state between the functions operating on it. If you do this then passing the state will not be your concern and your result could be either a tuple or a record:
Then your displayResult will look like this:
UPDATE
If you don't want the a command to output anything then the output field in Result could have the type "Maybe String" instead of String. \
Then you will have to choose only the Just outputs before you display the results.
Second your Command type is not properly designed
data Command = Command { name :: String
, key :: Maybe String
, value :: Maybe String
}Having the name as String is not cool. You could have a type something like
data CommandType = Get | Set...but still your command type has too many invalid combinations which the type system cannot help you with (for example: name="get", key="xyz", value="asdfasdf"). Your type should be designed such that you avoid these combinations and you make the type system work for you.
Given this I would define the Command type like this:
data Command = Invalid | End | Get String | Set String Stringif your type is like this then execCmd will become:
exec (Get key) = ....
exec (Set key value) = ...
exec Exit = ...
exec Invalid = ...If you only address these two issues you will see a lot of simplification in your code. Other things would be:
Using the State monad will help you with the passing of the current db state between the functions operating on it. If you do this then passing the state will not be your concern and your result could be either a tuple or a record:
data Result = Result { output: String ; end:Bool}Then your displayResult will look like this:
UPDATE
If you don't want the a command to output anything then the output field in Result could have the type "Maybe String" instead of String. \
data Result = Result { output: Maybe String ; end:Bool}Then you will have to choose only the Just outputs before you display the results.
Code Snippets
data Command = Command { name :: String
, key :: Maybe String
, value :: Maybe String
}data CommandType = Get | Setdata Command = Invalid | End | Get String | Set String Stringexec (Get key) = ....
exec (Set key value) = ...
exec Exit = ...
exec Invalid = ...data Result = Result { output: String ; end:Bool}Context
StackExchange Code Review Q#24741, answer score: 5
Revisions (0)
No revisions yet.