Search code examples
haskelldo-notation

concatenate getLine input with haskell array throws a type error


im totally beginner at Haskell i came from js environment , i have a simple array students into which i want to push some student objects but sadly Haskell does not support objects ( if there is a way i can do it please guide me ) so tried to make a simple program that reads the user input (array) and push that into the students array , here is what i have tried :

main :: IO()
main = do 
  let students = []
  studentArray <- getLine
  students ++ studentArray
  print(students)

but the following error is thrown : Couldn't match type `[]' with `IO'


Solution

  • First, you might want to take a look at the resources in this SO answer. If you haven't already worked through the tutorials in the section for "Absolute Beginner"s, that would be a good starting point.

    See, for other programming languages, it's usual to start with programs that print "Hello, world!" on the screen or that -- like your example -- read lists of students from the console and print them back out. For Haskell, it usually makes more sense to work with quite different types of programs at first. For example, the tutorial "Learn You a Haskell for Great Good" doesn't get to "Hello, world!" until Chapter 9, and the "Happy Learn Haskell Tutorial" doesn't get to it until Chapter 15 (and then it only covers output -- input doesn't come until Chapter 20).

    Anyway, back to your example. The problem is with the line students ++ studentArray. This is an expression that concatenates the empty list students = [] with the value of studentArray, which is a String retrieved by getLine. Since a String is just a list of characters, the empty list is just the empty string, so you are writing the rough equivalent of the JavaScript function:

    function main() {
        var students = ""          // empty list is just empty string
        var studentArray = readLineFromSomewhere()
        students + studentArray    // concatenate strings and throw away result
        console.log(students)      // print the empty string
    }
    

    In JavaScript, this would run and print the empty string because the students + studentArray line doesn't do anything. In Haskell, this doesn't type check because Haskell expects all (non-let) lines in this do block to be I/O actions:

    main :: IO ()         -- signature forces `do` block to be I/O
    main = do 
      let students = []          -- "let" line is okay
      studentArray <- getLine    -- `getLine` is IO action
      students ++ studentArray   -- **NOT** IO action:  it's a String AKA [Char]
      print students             -- `print students` is IO action
    

    Because students ++ studentArray is a String / [Char] / list of characters appearing in an IO do-block, Haskell expected an IO something but found a [something], and it's complaining that the types of lists ([]) and IO don't match.

    But, even if you could fix this, it wouldn't help because, like the JavaScript + operator and unlike the JavaScript push method, the Haskell ++ operator doesn't modify its arguments, so a ++ b only returns the concatenation of a and b without changing a or b.

    This is a pretty fundamental aspect of Haskell that makes it different from most other programming languages. By default, Haskell variables are immutable. Once they are assigned, at the top level, by a let statement, or assigned as arguments in a function call, they don't change value. (In fact, since they aren't really "variable", we usually call them "bindings" instead of "variables".) So, if you want to build up a list of students in Haskell, you don't start by assigning an empty list to a variable and then trying to modify that variable by adding students. Instead, you either do it all at once:

    import Control.Monad (replicateM)
    
    main :: IO ()
    main = do
      putStrLn "Enter number of students:"
      n <- readLn
      putStrLn $ "Enter " ++ show n ++ " student names:"
      students <- replicateM n getLine
      putStrLn $ "List of students:"
      print students
    

    or use function calls to simulate variables by re-binding an identifier to an updated value:

    main :: IO ()
    main = do
      putStrLn "Enter number of students:"
      n <- readLn
      putStrLn $ "Enter " ++ show n ++ " student names:"
      students <- getStudents n []
      print students
    
    getStudents :: Int -> [String] -> IO [String]
    getStudents 0 studentsSoFar = return studentsSoFar
    getStudents n studentsSoFar = do
      student <- getLine
      getStudents (n-1) (studentsSoFar ++ [student])
    

    See here how getStudents is originally called with the total number of students and an initial empty list (which get bound to n and studentsSoFar respectively in the getStudents call), and then uses recursion to re-bind n and studentsSoFar to decrement n while "pushing" more students on to studentsSoFar.

    By itself, the expression studentsSoFar ++ [student] would do nothing, but by using it in a recursive getStudents call, this new value can be re-bound as studentsSoFar to simulate changing the value of this "variable".

    Anyway, this is a pretty standard approach in Haskell, but it's maybe unusual for folks coming from JavaScript or other languages, so it's worth working through tutorials that cover recursion before input/output... like "Learn You" (recursion in Chapter 5, I/O in Chapter 9) or "Happy Learn" (recursion in Chapter 10, I/O in Chapters 15 and 20) or "Haskell Programming from First Principles" (recursion in Chapter 8, I/O in Chapter 29) or "Programming in Haskell" (recursion in Chapter 6, I/O in Chapter 10). I'm sure you see the pattern here.