Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

stdin

haskell

map

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.

like image 912
Ashton H. Avatar asked Dec 25 '22 17:12

Ashton H.


2 Answers

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.

like image 56
bheklilr Avatar answered Jan 05 '23 17:01

bheklilr


Your problem is that Map.insert does not do what map.remove does in C++. Map.insert returns a new Map which has the element in it but you are simply throwing this new Map away. This is how nearly all Haskell data structures work, for instance the code:

main = do
  let x = []
      y = 5 : x
  print x

prints the empty list []. The cons : operator does not destructively modify the empty list but returns a new list containing 5. Map.insert does the same but with Maps instead of lists.

like image 40
asm Avatar answered Jan 05 '23 17:01

asm