Haskell:如何逐行从stdin读取值并将其添加到映射中?

4

我想从标准输入中读取字符串并将它们存储到一个map中,其中key是输入字符串,value是此字符串出现的先前次数。在Java中,我会这样做:

for (int i = 0; i < numberOfLines; i++) {
    input = scanner.nextLine();
    if (!map.containsKey(input)) {
        map.put(input, 0);
        System.out.println(input);
    } else {
        int num = map.get(input) + 1;
        map.remove(input);
        map.put(input, num);
        System.out.println(input.concat(String.valueOf(num));
    }
}

我曾尝试使用Haskell并使用forM_,但没有成功。
import Control.Monad
import qualified Data.Map as Map
import Data.Maybe

main = do
    input <- getLine
    let n = read input :: Int
    let dataset = Map.empty
    forM_ [1..n] (\i -> do
        input <- getLine
        let a = Map.lookup input dataset
        let dataset' = 
            if isNothing a then
                Map.insert input 0 dataset
            else
                Map.insert input num (Map.delete input dataset)
                where num = ((read (fromJust a) :: Int) + 1)
        let dataset = dataset'
        let output = if isNothing a then
                input
            else
                input ++ fromJust a
        putStrLn output)

以上代码中的dataset内容根本没有改变。
3个回答

5
你的问题在于,在C++中,Map.insert不能像map.remove那样运作。 Map.insert返回一个新的Map,其中包含该元素,但你却抛弃了这个新的Map。 这几乎是Haskell所有数据结构的工作方式,例如下面的代码:
main = do
  let x = []
      y = 5 : x
  print x

打印空列表[]。Cons运算符:不会破坏空列表,但会返回一个包含5的新列表。 Map.insert也是如此,只不过用于Map而非列表。


5

Data.Map中定义的Map是一个不可变数据类型。调用Map.insert会返回修改后的Map,它不会改变你已经有的那一个。你想要做的是在循环中迭代地应用更新。更像这样:

import qualified Data.Map as M
import Data.Map (Map)


-- Adds one to an existing value, or sets it to 0 if it isn't present
updateMap :: Map String Int -> String -> Map String Int
updateMap dataset str = M.insertWith updater str 0 dataset
    where
        updater _ 0 = 1
        updater _ old = old + 1

-- Loops n times, returning the final data set when n == 0
loop :: Int -> Map String Int -> IO (Map String Int)
loop 0 dataset = return dataset
loop n dataset = do
    str <- getLine
    let newSet = updateMap dataset str
    loop (n - 1) newSet -- recursively pass in the new map

main :: IO ()
main = do
    n <- fmap read getLine :: IO Int -- Combine operations into one
    dataset <- loop n M.empty -- Start with an empty map
    print dataset

请注意,这实际上是更少的代码(如果您只计算出现次数,则代码会更短),并且将纯代码与不纯代码分离。在此情况下,您实际上不想使用forM_,因为计算的每个步骤都依赖于前一个步骤。最好编写一个递归函数,在条件下退出。如果您愿意,还可以将loop编写为:
loop :: Int -> IO (Map String Int)
loop n = go n M.empty
    where
        go 0 dataset = return dataset
        go n dataset = getLine >>= go (n - 1) . updateMap dataset

在这里,我将旧的loop的代码压缩成一行,然后将其放入go中,这使得你可以通过以下方式调用它:

main :: IO ()
main = do
    n <- fmap read getLine :: IO Int
    dataset <- loop n
    print dataset

这样可以避免您必须在第一次调用时传递M.emptyloop,除非您有在同一地图上多次调用loop的用例。

非常感谢您的全面解释!我只有一个问题关于您的代码:值增量是如何工作的?就我所知,如果同一字符串出现多次,它将始终返回1 - Ashton H.
@AshtonHearts 您是正确的,看起来我犯了一个错误,没有经过足够的测试就没有发现。看起来我的更新器参数顺序错了。我会修复它。 - bheklilr

0
关于您的Java代码,首先,您不需要在插入新值之前从地图中删除。
关于Haskell,该语言的工作方式与您想象的不同:您的let技巧并未更新值,在Haskell中基本上所有内容都是不可变的。
只使用基本的getLine,一种方法是使用递归:
import qualified Data.Map as Map


type Dict = Map.Map String Int

makeDict ::Dict -> Int -> IO Dict
makeDict d remain = if remain == 0 then return d else do
  l <- getLine
  let newd = Map.insertWith (+) l 1 d
  makeDict newd (remain - 1)

newDict count = makeDict Map.empty count

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接