patternMinor
"Log in", extract number from a map, increment it, and log out
Viewed 0 times
mapnumberincrementlogextractandfromout
Problem
Basically, you just run
It all compiles and works fine, but how can I decouple this code, and make it more composable? I'm finding it's difficult to separate concerns, ex between the logic, and the IO + State effects.
What would be a better, more idiomatic way to right this, which would allow me to add features, such as a password, or decrement option?
Possible Actions:
```
{-# LANGUAGE FlexibleContexts #-}
import System.IO
import Data.Map
import Control.Monad.State
import Control.Lens.Tuple
import Control.Lens.Setter
import Control.Lens.Getter
import Prelude hiding (lookup) -- Data.Map has one I want
-- Features:
-- User can login, see their number, increment it ad lib, and log out
--
-- login
-- new username --> new map entry
-- old username --> display map entry
-- inc - increment current user's entry in map
-- logout - empties the current username
-- stop - close IO
-- bad input - just ask for another action
type DB = Map String Int
type Username = String
type AppState = (Username, DB)
db = fromList [("a", 0),
("b", 99),
("c", 152)] :: DB
-- increment a key in a map
incDB :: String -> DB -> DB
incDB k = (update (\x -> Just (x + 1)) k)
-- λ> inc "a" $ inc "a" $ inc "a" db
-- increment current user in a State Monad
-- (Num a, Ord k, MonadState (k, (Map k a)) m) => m x
incUser :: (MonadState AppState m) => m ()
incUser = do user runState incUser ("b", db)
-- 1. update logged-in user name, 2. add new entry to db if it's a new user
login :: (MonadState AppState m) => String -> m ()
login newuser = do modify $ set _1 newuser
modify $ over _2 $ insertWithKey (\k new old -> old) newuser 0 -- inserts new user if necessary
-- λ> runState (login "d") ("", db)
-- ((),("d",fromList [("a",0),("b",99),("c",152),("d",0)]))
--
main and it allows the client to "log in", and extract their number from a map, increment their own number, and log out.It all compiles and works fine, but how can I decouple this code, and make it more composable? I'm finding it's difficult to separate concerns, ex between the logic, and the IO + State effects.
What would be a better, more idiomatic way to right this, which would allow me to add features, such as a password, or decrement option?
Possible Actions:
login name, inc, get, logout```
{-# LANGUAGE FlexibleContexts #-}
import System.IO
import Data.Map
import Control.Monad.State
import Control.Lens.Tuple
import Control.Lens.Setter
import Control.Lens.Getter
import Prelude hiding (lookup) -- Data.Map has one I want
-- Features:
-- User can login, see their number, increment it ad lib, and log out
--
-- login
-- new username --> new map entry
-- old username --> display map entry
-- inc - increment current user's entry in map
-- logout - empties the current username
-- stop - close IO
-- bad input - just ask for another action
type DB = Map String Int
type Username = String
type AppState = (Username, DB)
db = fromList [("a", 0),
("b", 99),
("c", 152)] :: DB
-- increment a key in a map
incDB :: String -> DB -> DB
incDB k = (update (\x -> Just (x + 1)) k)
-- λ> inc "a" $ inc "a" $ inc "a" db
-- increment current user in a State Monad
-- (Num a, Ord k, MonadState (k, (Map k a)) m) => m x
incUser :: (MonadState AppState m) => m ()
incUser = do user runState incUser ("b", db)
-- 1. update logged-in user name, 2. add new entry to db if it's a new user
login :: (MonadState AppState m) => String -> m ()
login newuser = do modify $ set _1 newuser
modify $ over _2 $ insertWithKey (\k new old -> old) newuser 0 -- inserts new user if necessary
-- λ> runState (login "d") ("", db)
-- ((),("d",fromList [("a",0),("b",99),("c",152),("d",0)]))
--
Solution
I would inline, eta-reduce and use more library functions to make the code short enough that adding another command is trivial. Perhaps give the fields names so adding another can't mess up numbering, and you need less comments because the code describes itself.
Edit: I'll use non, making the map not add a value for new users until they want to change it. This way we don't need to initialize with
Edit: I'll use non, making the map not add a value for new users until they want to change it. This way we don't need to initialize with
db . at user %= ( Just 0) on logging in, and we can get rid of the Just returned by the "get" action without needing to promise we already initialized. (Which we don't necessarily have, as the initial ""!){#- LANGUAGE TemplateHaskell, LambdaCase -#}
import Control.Lens -- batteries included
import Control.Monad.Trans.Maybe
import Control.Applicative -- (), (), empty, ()
import qualified Data.Map as M -- Data.Map's name collisions with prelude are customarily handled by qualification
data AppState = AppState
{ _appStateUsername :: String
, _appStateDb :: M.Map String Int
}
makeFields ''AppState
-- Here's one we can actually outline, because it's used more than once, and it allows us to pull out the liftIO, and it shows us that we forgot the hFlush and the trailing ' ' one of the times.
prompt :: MonadIO m => String -> m String
prompt s = liftIO $ do
putStr $ s ++ ": "
hFlush stdout
getLine
main = runMaybeT $ (`execStateT` AppState "" M.empty) $ forever $ do
prompt "next action" >>= \case
"inc" -> do
user do
user do
n use username use db
liftIO $ putStrLn $ "current num: " ++ show n -- Note that this includes the Maybe, even though at this point we're always just, unless we're still the initial "".
"logout" -> do
username .= "" -- Note that this makes for shenanigans if someone logs in as "".
"stop" -> empty -- MaybeT helps us out with the control flow to get rid of the special case and recursion.
_ -> liftIO $ putStrLn $ "usage: ehhh just look at the code mmkay"Code Snippets
{#- LANGUAGE TemplateHaskell, LambdaCase -#}
import Control.Lens -- batteries included
import Control.Monad.Trans.Maybe
import Control.Applicative -- (<$>), (<*>), empty, (<|>)
import qualified Data.Map as M -- Data.Map's name collisions with prelude are customarily handled by qualification
data AppState = AppState
{ _appStateUsername :: String
, _appStateDb :: M.Map String Int
}
makeFields ''AppState
-- Here's one we can actually outline, because it's used more than once, and it allows us to pull out the liftIO, and it shows us that we forgot the hFlush and the trailing ' ' one of the times.
prompt :: MonadIO m => String -> m String
prompt s = liftIO $ do
putStr $ s ++ ": "
hFlush stdout
getLine
main = runMaybeT $ (`execStateT` AppState "" M.empty) $ forever $ do
prompt "next action" >>= \case
"inc" -> do
user <- use username
db . at user . non 0 += 1
"login" -> do
user <- prompt "enter username"
username .= user
"get" -> do
n <- M.findWithDefault 0 <$> use username <*> use db
liftIO $ putStrLn $ "current num: " ++ show n -- Note that this includes the Maybe, even though at this point we're always just, unless we're still the initial "".
"logout" -> do
username .= "" -- Note that this makes for shenanigans if someone logs in as "".
"stop" -> empty -- MaybeT helps us out with the control flow to get rid of the special case and recursion.
_ -> liftIO $ putStrLn $ "usage: ehhh just look at the code mmkay"Context
StackExchange Code Review Q#136599, answer score: 3
Revisions (0)
No revisions yet.