Reputation: 367
I'm a newbie in haskell and start to write my first small program.
The purpose: The app asks the user to input items for some categories. All inputs are stored in a Map Category [Item] (or Map String [String]). Once all categories has been fulfilled, the map is written to a file.
import qualified Data.Map as Map
type InputLine = IO [String]
type ItemsRegistry = Map.Map String InputLine
--main = do sequence . printValues . getInput $ ["head", "body"]
main = do
writeFile "data.txt" ""
putStrLn "Write the word \"stop\" to end the input"
sequence . writeData "data.txt" . getInput $ ["B", "A"]
getInput :: [String] -> ItemsRegistry
getInput cat = foldl (\acc category -> Map.insert category (insertCategory category) acc) Map.empty cat
insertCategory :: String -> InputLine
insertCategory category = do
putStrLn $ "Add items for category " ++ category
insertItem []
insertItem :: [String] -> InputLine
insertItem values = do
x <- getLine
case x of "stop" -> return values
x -> insertItem (x:values)
writeData :: String -> ItemsRegistry -> [IO ()]
writeData path data_ = [ writeLine path k (Map.lookup k data_) | k <- Map.keys data_ ]
-- filepath category lineOfItems
writeLine :: String -> String -> Maybe InputLine -> IO ()
writeLine path category (Just line) = line >>= (\words -> appendFile path $ formatLine category (unwords words))
writeLine path category Nothing = return ()
formatLine :: String -> String -> String
formatLine category items = category ++ " " ++ items ++ "\n"
--printValues :: ItemsRegistry -> [IO ()]
--printValues l = [ v >>= putStrLn . show | (k,v) <- Map.toList l]
When running the above I can't figure out why it asks me for input category A before B since I used a foldl ? Seems like there is some lexical ordering behind it but I can't understand why.
Thanks
Upvotes: 0
Views: 179
Reputation: 29193
Map
s are ordered. That's why most of the functions that work on Map k v
s have a Ord k
constraint. I think, internally, they are kept as some sort of binary tree. There is no reordering of execution, just reordering of data. If you want a "map-like" structure that stays in the order you want rather than being sorted, use association lists:
type Assoc k v = [(k, v)]
-- usually we don't use this alias; [(k, v)] is shorter and widely understood
In Prelude
, you will find
lookup :: Eq a => k -> [(k, v)] -> Maybe v
and all other map-like operations can be implemented through basic list manipulation.
In your code, you should thus change:
type ItemRegistry = [(String, InputLine)]
Also, if you use foldl
to write some function from lists to lists, it will reverse the order, generally. This is because
foldl c n [x, y, z] = c (foldl c n [x, y]) z
which says to do something with the last element before doing something with the rest of the list, while
foldr c n [x, y, z] = c x (foldr c n [x, y])
means to do something with the first element before processing the rest. If you are used to strict languages, this is backwards. In a lazy language, things on the "outside" of an expression "happen" first; in a strict language things on the "inside" happen first.
This was not a problem for Map
, because Map.insert
is unordered (as long as there are no duplicate keys); it doesn't matter in what order you insert keys into a Map
, because the Map
will always be sorted according to the Ord
instance. It is, however, a problem for association lists, which preserve order. You should thus say:
getInput :: [String] -> ItemsRegistry
-- getInput = foldr (\category acc -> (category, insertCategory category) : acc) []
-- but that should really be
getInput = map (\category -> (category, insertCategory category))
-- which you can turn into
-- getInput = map ((,) <*> insertCategory)
-- if you want
Finally, your writeLine
and writeData
are questionable. Notice:
writeData path data_ = [ writeLine path k (Map.lookup k data_) | k <- Map.keys data_ ]
k
is definitely a key to data_
, but Map.lookup k data_ :: Maybe InputLine
. Why? It won't ever be Nothing
. The same would happen for Data.List.lookup
. This means you've done something wrong, and I'm sure it made you uncomfortable when you saw it, too. I see you band-aided it by making writeLine
accept a Maybe
, making it a no-op if it receives Nothing
, but that's just dodgy.
If you were using Map
(which you can't, really, as I've said), you would use this function, which turns a Map
into an association list.
-- alias toAscList
Data.Map.assocs :: Map k a -> [(k, a)]
And you would say
writeData path data_ = [ writeLine path k v | (k, v) <- assocs data_ ]
(If you think this look inefficient, it isn't. The documentation for assocs
says it is subject to list fusion, meaning that the intermediate assocs data_
list that appears to be created here gets optimized out of existence.)
Of course, we already have an association list. This makes writeData
even simpler:
-- do be careful; Map.insert clobbers duplicates, but (:)ing onto an association
-- list means duplicate keys are preserved
-- you'd need to run nubBy ((==) `on` fst) on data if that's an issue
-- *that* might be inefficient (O(n^2) in the length of data_)
writeData path data_ = [ writeLine path k v | (k, v) <- data_ ]
-- or even
-- writeData path = map (uncurry (writeLine path))
-- if you want to be confusing
-- writeData = map . uncurry . writeLine
writeLine :: String -> String -> InputLine -> IO ()
writeLine path category lines = do words <- lines
appendFile path $ formatLine category $ unwords words
Upvotes: 1
Reputation: 18249
According to the docs for Data.Map, Map.keys
returns "all keys of the map in ascending order". (Recall that the Map
type requires its keys to be of a type that's an instance of Ord
.)
Since "A"
comes before "B"
in the natural order, I think your use of Map.keys
is causing writeData
to have the "ask for A" action before the "ask for B" one.
Upvotes: 2