Behtx3
Behtx3

Reputation: 57

Haskell Input to create a String List

I would like to allow a user to build a list from a series of inputs in Haskell.

The getLine function would be called recursively until the stopping case ("Y") is input, at which point the list is returned.

I know the function needs to be in a similar format to below. I am having trouble assigning the correct type signatures - I think I need to include the IO type somewhere.

getList :: [String] -> [String]
getList list =  do line <- getLine
                   if line ==  "Y"
                      then return list
                      else getList (line : list)

Upvotes: 2

Views: 2886

Answers (1)

CR Drost
CR Drost

Reputation: 9817

So there's a bunch of things that you need to understand. One of them is the IO x type. A value of this type is a computer program that, when later run, will do something and produce a value of type x. So getLine doesn't do anything by itself; it just is a certain sort of program. Same with let p = putStrLn "hello!". I can sequence p into my program multiple times and it will print hello! multiple times, because the IO () is a program, as a value which Haskell happens to be able to talk about and manipulate. If this were TypeScript I would say type IO<x> = { run: () => Promise<x> } and emphatically that type says that the side-effecting action has not been run yet.

So how do we manipulate these values when the value is a program, for example one that fetches the current system time?

The most fundamental way to chain such programs together is to take a program that produces an x (an IO x) and then a Haskell function which takes an x and constructs a program which produces a y (an x -> IO y and combines them together into a resulting program producing a y (an IO y.) This function is called >>= and pronounced "bind". In fact this way is universal, if we add a program which takes any Haskell value of type x and produces a program which does nothing and produces that value (return :: x -> IO x). This allows you to use, for example, the Prelude function fmap f = (>>= return . f) which takes an a -> b and applies it to an IO a to produce an IO b.

So It is so common to say things like getLine >>= \line -> putStrLn (upcase line ++ "!") that we invented do-notation, writing this as

do 
    line <- getLine
    putStrLn (upcase line ++ "!")

Notice that it's the same basic deal; the last line needs to be an IO y for some y.

The last thing you need to know in Haskell is the convention which actually gets these things run. That is that, in your Haskell source code, you are supposed to create an IO () (a program whose value doesn't matter) called Main.main, and the Haskell compiler is supposed to take this program which you described, and give it to you as an executable which you can run whenever you want. As a very special case, the GHCi interpreter will notice if you produce an IO x expression at the top level and will immediately run it for you, but that is very different from how the rest of the language works. For the most part, Haskell says, describe the program and I will give it to you.

Now that you know that Haskell has no magic and the Haskell IO x type just is a static representation of a computer program as a value, rather than something which does side-effecting stuff when you "reduce" it (like it is in other languages), we can turn to your getList. Clearly getList :: IO [String] makes the most sense based on what you said: a program which allows a user to build a list from a series of inputs.

Now to build the internals, you've got the right guess: we've got to start with a getLine and either finish off the list or continue accepting inputs, prepending the line to the list:

getList = do
    line <- getLine
    if line == 'exit' then return []
                      else fmap (line:) getList

You've also identified another way to do it, which depends on taking a list of strings and producing a new list:

getList :: IO [String]
getList = fmap reverse (go []) where 
    go xs = do 
        x <- getLine
        if x == "exit" then return xs
                       else go (x : xs)

There are probably several other ways to do it.

Upvotes: 4

Related Questions