1. 9
  1.  

  2. 1

    This always bothered me about haskell. Why is it it so hard to read a line from a file? I understand that because of jumping through all of these hula hoops we end up in situations where the compiler can suddenly magically do very cool things, but it feels like so much effort. I’m sure it’s effortless after you’ve internalized the state monad and the io monad and the writer monad and the reader monad, but until then, it still feels like a pain.

    1. 7

      This has nothing to do with reading a file. There is nothing about the IO monad in the article.

      Reading a file is not hard:

      main = do
        content <- readFile "/tmp/test.txt"
        let firstLine = head (lines content)
        putStrLn firstLine
      

      Done.

      Where was the effort?

      1. 5

        Since my above Haskell is actually async, let’s talk about real effort:

        var fs = require('fs');
        
        fs.readFile('/tmp/test.txt', 'utf8', function(err, content) {
            var firstLine;
        
            if(err)
                return console.error('Error loading file');
        
            firstLine = content.split(/\n/)[0];
            console.log(firstLine);
        });
        
        1. 3

          This also seems like a pain in the ass, I would not like to do IO things in this language either.

        2. 1

          If I didn’t want to putStrLen at the end, but instead wanted to do something else, would it be so easy? I might be misunderstanding, but my impression is that putStrLn eases my way here because putStrLen wraps firstLine in the IO monad for me.

          1. 2

            I think you might be misunderstanding. The putStrLn function takes a String and returns an IO action. What other than printing to the screen would you like to do? The last line could be anything!

            main = do
              content <- readFile "/tmp/test.txt"
              let firstLine = head (lines content)
              name <- getName
              printMessage name firstLine
            
            getName = do
              putStr "What is your name? "
              readLn
            
            printMessage name line =
              putStrLn ("You are " ++ name ++ " and the first line is " ++ line)
            
            1. 1

              I must be misunderstanding the type constraints. My impression is that I need the do loop to end with an IO. What if I wanted to parse the text file and return the sum of a bunch of numbers in the text file? If I could figure out how to wrap the number in an IO monad again, that would make it typecheck.

              I could just be badly misunderstanding do loops.

              1. 1

                You’re after the return function. Will take any value and wrap it in IO.

                1. 2

                  Gotcha. I guess I just had too much trouble with the haskell docs. My impression was that I could find everything I needed to know about IO here, but clearly I should have used my google-fu and ended up here. Thanks for the clarification!

                  That definitely makes the IO monad easier to work with, although it still seems onerous to me. I think I’ll have to force myself to do more with it in order to internalize how it works before it seems simple.

        3. 4

          It’s not hard to read lines from a file as Brian showed. Have you used Haskell before or do you just think that it’s hard to do trivial tasks like that?

          1. 1

            My main experience with the IO monad was trying to parse commandline options, which I found deeply frustrating. Definitely part of the problem was my inexperience with haskell, since before this I had only done trivial work in it before. However, it seems unreasonable to me that it should be so complicated.

            More precisely, I like that in Scala I can just look up io.Source.fromFile(“filename”).getLines (or something similar) and it hands me back an iterator, instead of an IO iterator which I have to be super careful with, especially since it isn’t clear to me how to unwrap an IO a la

            val x = unwrap IO(lines)

            edit: I am aware that my example of having a hard time with the IO monad is godawful haskell.

            1. 2

              You can’t completely unwrap IO, that’s the point. You always want to wrap it back up with map or flatMap/bind/chain. That’s exactly what do-notation does:

               main = do
                 content <- readFile "/tmp/test.txt"
                 -- "content" as being unwrapped
                 let x = lines content
                 -- wrap it back up with "print"
                 print x
              
              1. 1

                I am having trouble understanding how I do this:

                read data from a file
                get out of io land with that data
                manipulate that data
                later, output the result of manipulating the data.

                The impression I have now is that it is illegal to leave IO land with that data, unless it is safely wrapped in an IO. If I wanted to do something like this, instead the right way would be to say something along the lines of

                let x = read data  
                let y = map x manipulate  
                -- I don't know map syntax in haskell  
                do  
                  manipulated <- y  
                  putStrLn manipulated  
                

                it seems like a pain to have to add an extra level of mapping every time I want to manipulate something that came from outside. I have heard that this is to protect us from the outside world since it is impure, but I suppose I just haven’t internalized it.

                1. 1

                  The above example does almost the 4 things you listed. Let’s go line by line:

                  main = do
                  

                  read data from a file

                  content <- readFile "/tmp/test.txt"
                  

                  Now here’s the thing – content is not an IO value but it’s also not “out of IO land” because it can’t escape (the whole do expression must return an IO).

                  manipulate that data

                  let x = lines content
                  

                  later, output the result of manipulating the data.

                  print x
                  
                  1. 1

                    I understood that part. What I didn’t understand how to do was how to carry around the IO in a subsequent expression.

                    1. 3

                      Maybe this will help clarify things:

                      import Data.Char
                      
                      shout :: String -> String
                      shout line = map toUpper line
                      
                      getLines :: String -> IO [String]
                      getLines filename =
                        do content <- readFile filename
                           let ls = lines content
                           let shoutyLines = map shout ls
                           return shoutyLines
                      
                      main :: IO ()
                      main =
                        do shoutyLines <- getLines "example.in"
                           let joinedLines = unlines shoutyLines
                           putStrLn joinedLines
                      

                      getLines :: String -> IO [String]

                      getLines takes in a String and, if it is ever run, does some IO and returns a list of Strings. In this case, if it is run, it reads all the lines from a file into a list of Strings, converts each of those strings to uppercase and returns (lifts it into a Monad context) the new list.

                      By the way, the above code could also be written as (and it actually gets expanded into something very similar since do notation is just syntactic sugar):

                      import Data.Char
                      
                      shout :: String -> String
                      shout line = map toUpper line
                      
                      getLines :: String -> IO [String]
                      getLines filename =
                        readFile filename >>= \content ->
                          return $ map shout $ lines content
                      
                      main :: IO ()
                      main =
                        getLines "example.in" >>= \shoutyLines ->
                          putStrLn $ unlines shoutyLines