使用折叠的字符串排序

时间:2019-04-25 12:45:37

标签: haskell

我是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?似乎后面有一些词汇顺序,但我不明白为什么。

谢谢

2 个答案:

答案 0 :(得分:3)

根据Data.Map的docsMap.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

最后,您的writeLinewriteData值得怀疑。注意:

writeData path data_ = [ writeLine path k (Map.lookup k data_) |  k <- Map.keys data_ ]

k绝对是data_的键,但是Map.lookup k data_ :: Maybe InputLine。为什么?永远不会是NothingData.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