我是haskell的新手,开始写我的第一个小程序。
目的:该应用程序要求用户输入某些类别的项目。所有输入都存储在Map Category [Item](或Map String [String])中。完成所有类别后,将地图写入文件。
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]
运行上述命令时,由于使用了折页笔,我不知道为什么为什么要在B之前要求输入类别A?似乎后面有一些词汇顺序,但我不明白为什么。
谢谢
答案 0 :(得分:3)
根据Data.Map的docs,Map.keys
返回“地图的所有键以升序排列”。 (回想一下,Map
类型要求其键的类型必须是Ord
的实例。)
由于"A"
按照自然顺序排在"B"
之前,我认为您对Map.keys
的使用导致writeData
在“要求B“一个。
答案 1 :(得分:2)
Map
是有序的。这就是为什么在Map k v
上运行的大多数函数都有Ord k
约束的原因。我认为,在内部,它们被保存为某种二叉树。没有执行的重新排序,只是数据的重新排序。如果要使“类似地图的”结构保持所需顺序而不是进行排序,请使用关联列表:
type Assoc k v = [(k, v)]
-- usually we don't use this alias; [(k, v)] is shorter and widely understood
在Prelude
中,您会找到
lookup :: Eq a => k -> [(k, v)] -> Maybe v
和所有其他类似地图的操作都可以通过基本的列表操作来实现。
因此,您应该在代码中进行更改:
type ItemRegistry = [(String, InputLine)]
此外,如果您使用foldl
从列表到列表编写某些函数,通常它将颠倒顺序。这是因为
foldl c n [x, y, z] = c (foldl c n [x, y]) z
表示先处理最后一个元素,然后再处理列表的其余部分,而
foldr c n [x, y, z] = c x (foldr c n [x, y])
表示在处理其余元素之前先对第一个元素进行处理。如果您习惯于严格的语言,这是倒退。用懒惰的语言来说,首先是表达式“ happen”的“外部”上的内容;用严格的语言来说,“内部”的事情首先发生。
这对于Map
来说不是问题,因为Map.insert
是无序的(只要没有重复的键);将密钥插入Map
的顺序无关紧要,因为Map
将始终根据Ord
实例进行排序。但是,这对于保留顺序的关联列表是一个问题。因此,您应该说:
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
最后,您的writeLine
和writeData
值得怀疑。注意:
writeData path data_ = [ writeLine path k (Map.lookup k data_) | k <- Map.keys data_ ]
k
绝对是data_
的键,但是Map.lookup k data_ :: Maybe InputLine
。为什么?永远不会是Nothing
。 Data.List.lookup
也会发生同样的情况。这意味着您做错了什么,而且我敢肯定,当您看到它时,也会使您感到不舒服。我看到您通过让writeLine
接受Maybe
使其成为乐队的助手,如果收到Nothing
则使它成为空操作,但这只是狡猾。
如果您使用的是Map
(实际上,正如我所说的那样,您实际上不能这样做),则可以使用此功能,它将Map
转换为关联列表。
-- alias toAscList
Data.Map.assocs :: Map k a -> [(k, a)]
你会说
writeData path data_ = [ writeLine path k v | (k, v) <- assocs data_ ]
(如果您认为这看起来效率不高,那不是。assocs
的文档说它受列表融合的约束,这意味着似乎在此处创建的中间assocs data_
列表已得到优化。不存在。)
当然,我们已经拥有一个关联列表。这使writeData
更加简单:
-- 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