2

Finally learning how to use monads in Haskell!

I want to read a file testInput, drop the first line, apply the function waffles to every other line, and save the result in a file output.txt.

I have written the following code:

main = do contents <- tail . fmap lines . readFile $ "testInput" result <- fmap waffles contents writeFile "output.txt" $ concat result waffles row col = (row - 1)*(col - 1) 

Sadly the compiler complains:

waffles.hs:3:41: Couldn't match type ‘IO String’ with ‘[String]’ Expected type: FilePath -> [String] Actual type: FilePath -> IO String In the second argument of ‘(.)’, namely ‘readFile’ In the second argument of ‘(.)’, namely ‘fmap lines . readFile’ waffles.hs:5:9: Couldn't match expected type ‘[b]’ with actual type ‘IO ()’ Relevant bindings include program :: [b] (bound at waffles.hs:2:1) In a stmt of a 'do' block: writeFile "output.txt" $ concat result In the expression: do { contents <- tail . fmap lines . readFile $ "testInput"; result <- fmap waffles contents; writeFile "output.txt" $ concat result } In an equation for ‘program’: program = do { contents <- tail . fmap lines . readFile $ "testInput"; result <- fmap waffles contents; writeFile "output.txt" $ concat result } Failed, modules loaded: none. 

I find that error quite daunting. Can you help me debug it?

I also would appreciate code style advice!

EDIT: I forgot to split the lines of the file and convert them to integers. I tried solving that as follows:

main = do contents <- tail . fmap lines . readFile $ "testInput" contents <- fmap read . words contents result <- fmap waffles contents writeFile "output.txt" $ concat result waffles row col = (row - 1)*(col - 1) 

But that only introduced more confusing compiler errors.

9
  • 3
    I'll give you a hint for the first error: because you're composing tail with fmap, the compiler assumes you meant fmap to refer to []'s version of fmap (fmap lines :: [String] -> [[String]]), not IO's. Commented Apr 25, 2018 at 14:15
  • 1
    Nice one! For the second error, have a think about what you expect the type of contents to be in the expression fmap waffles contents. (I wish Haskell had a decent interactive IDE so you could look at the types of variables in real time.) Commented Apr 25, 2018 at 14:20
  • 1
    @BenjaminHodgson ghci has a not-decent way of achieving that. For example, here if I :set -fdefer-type-errors; :set +c; :l test.hs; :type-at test.hs 2 4 2 13 (that last command has format start-line, start-column, end-line, end-column) I get :: [String]. But it's super finicky, doesn't appear to coordinate well with :r (or even with later :l commands), and having to muck about in your editor to get line and column numbers sucks. The :all-types command is significantly more verbose and less interactive, but ironically is more usable in some ways... Commented Apr 25, 2018 at 14:47
  • 1
    @DanielWagner Neat trick but not a very good UI! If the questioneer had been able to hover over an identifier and see a popup with the type, they probably would have been able to fix their own problem and wouldn’t have needed to even come to Stack Overflow Commented Apr 25, 2018 at 15:14
  • 1
    "Well contents should be a IO String type" Several errors here. One, the <- you are using is not for nothing, it gets you inside the monad so you don't have to fmap over it yourself. Two, from lines you get a [String] not a String. Three, waffles accepts two arguments, how exactly do you plan to use it to map over a String (or IO String, or [String], or whatever) ? Four, these arguments are numbers, you are not supposed to multiply strings. Commented Apr 25, 2018 at 15:35

1 Answer 1

4

The first line in your do statement fails because you are trying to use tail on an IO [String]. You need to fmap the tail function:

contents <- fmap tail . fmap lines . readFile $ "testInput" -- or.... contents <- fmap (tail . lines) . readFile $ "testInput" 

Now you need a way to get every other line from contents. You could define a simple everyOther function for this:

everyOther :: [a] -> [a] everyOther (x:_:xs) = x : everyOther xs everyOther _ = [] 

And now you can chain that into your fmap in the first line:

contents <- fmap (everyOther . tail . lines) . readFile $ "testInput" 

Your waffles function of (row - 1)*(col - 1) does not seem related to what I believe the type signature should be. Try starting with a type signature and building waffles from their. Based on your description, you are simply providing every other line to the function, so it should have signature:

waffles :: String -> String 

Given that type signature for waffles, you can apply it via:

let result = fmap waffles contents 

One more thing on the output: concat will smush all lines together. You probably want line breaks in there, so you might want to use unlines instead.

main = do contents <- fmap (everyOther . tail . lines) . readFile $ "testInput" let result = fmap waffles contents writeFile "output.txt" $ unlines result 
Sign up to request clarification or add additional context in comments.

3 Comments

The reason you need the "fmap" is that "readFile" gives you an "IO String" (i.e the string is inside the IO monad). You can't get the string out of the IO, but you can reach inside the IO and do stuff to the string in it, just like you can with any other container. "fmap" is the function that does this.
I think fmap (tail . fmap) is supposed to be fmap (tail . lines).
Thanks @DarthFennec. I fixed the mistake

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.