Haskell: how to read values from stdin line-by-line and add them to a map?

dictionary, haskell, stdin

Solution

The `Map` defined in `Data.Map` is an immutable data type. Calling `Map.insert` returns a modified `Map`, it does not change the one you already have. What you want to do is iteratively apply updates in a loop. Something more like

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

Notice how this is actually less code (it's be even shorter if you just counted the number of occurrences, then `updateMap dataset str = M.insertWith (+) str 1 dataset`), and it separates the pure code from the impure.

In this case, you don't actually want to use `forM_`, because each step of the computation depends on the previous. It's preferred to write a recursive function that exits at a condition. If you so desired, you could also write `loop` as

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

Here I've compressed the body of the old `loop` into a single line and then put it inside `go`, this allows you to call it as

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

This removes the need to know that you must pass in `M.empty` into `loop` for the first call, unless you have a use case to call `loop` multiple times on the same map.

Problem

I want to read strings from stdin and store them into a map, where `key` is the input string and `value` is the number of previous occurrences of this string. In Java I would have done something like this: ``` 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)); } } ``` I've tried doing the same in Haskell by using `forM_` but had no luck. ``` 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) ``` The contents of `dataset` in the above code does not change at all.

Original source