Alright- So what do we know about the people at our picnic?Now that we've got "Hello World" working... let's load the answers people gave to the questions on the picnicmob.org website... For testing purposes, I've created some anonymized data and put it into the file people.txt for you to use- Just save this file to your computer. Here's what that file looks like- It's just a list of list of numbers. Each row is a person and the numbers are the value answer number they gave on each question: |
[ [2,3,3,4,4,3,5,2,2,3,2,2,2,3,2,5,3,1,3,5,2,5,2,2,2,3,2,5,2,3], [2,3,3,2,3,3,5,2,3,4,2,2,1,1,1,2,1,5,1,4,2,5,2,2,2,2,4,1,1,1], [2,3,4,3,3,5,5,2,3,5,2,3,2,1,2,4,4,3,3,1,2,5,3,5,2,3,4,1,1,2], [1,3,3,3,3,3,3,3,4,4,3,3,4,4,2,3,3,3,1,4,3,4,3,3,2,1,3,2,3,3], [4,1,3,3,3,3,4,1,3,4,3,3,1,2,1,4,4,2,2,4,2,2,2,1,2,3,4,2,5,1], [2,1,1,2,3,5,4,1,1,1,2,3,5,1,3,2,4,2,3,5,2,5,2,2,2,3,4,2,2,4], [1,3,4,2,3,5,4,1,3,1,2,2,2,3,1,1,1,2,2,2,2,1,2,1,2,3,3,1,3,3], [3,3,3,1,3,2,2,1,2,3,2,3,2,3,2,3,3,5,2,1,2,5,2,1,2,1,4,2,2,2], [1,3,4,3,3,5,5,3,1,4,2,4,1,1,5,1,1,4,2,5,2,5,3,1,2,3,4,2,1,4], [1,3,4,2,4,5,4,3,2,1,3,4,1,1,4,5,4,5,3,4,2,2,2,1,2,3,3,4,1,4], [1,1,1,4,4,1,4,3,3,3,4,3,1,2,1,4,1,5,2,2,2,4,2,5,2,1,4,4,1,3], ...
Here's the code that reads in this list of numbers. Adding this code to our existing program is super easy: Just paste this new code to the bottom of the "Hello World" program to make the new code work- Do the same thing, as we go along, with all the other code fragments in this tutorial. people_text <- readFile "people.txt" let people :: [Person] people = read people_text putStr "Number of people coming: " print (length people) Now let's see if this code is able to read our file: > runHaskell tutorial.hs Hello World! Let's have a picnic! Number of people coming: 200 perfect!! Ok, so let's dissect this code to figure out how it worked... The first line read the file "people.txt" and assigned it to the a string named people_text... Note that we used a left-pointing arrow <- to do the assignment, instead of an equals sign. The reason for this is that Haskell is very finicky about code that does I/O, like reading from the disk or printing something on the screen. In the Haskell way of thinking, code that does this is pure evil and needs to be imprisoned into its own pen, separately from the rest of your Haskell program.
Why does it think I/O is evil? It has to do with those types we defined at the top of our program: For instance, we said that the definition of an EnergyFunction is a function that converts from an arbitrary type a to an Int- That is all that an EnergyFunction is allowed to "do". In Haskell's way of thinking, if an EneregyFunction, let's say, prints something on the screen, then that means it's doing more than we said it would do, and will complain when we try to compile our program. If we want to "break the rules" of Haskell and do stuff like read from the disk or write to the screen, we have to do it in a special way: We put the word do at the beginning of the function (remember the line "main = do"?) and whenever we grab some value using IO, we use the aforementioned <- arrow and it will let us do what we want. ***IMPORTANT: Before starting to read this next paragraph, put your fingers in both ears and chant "Lalalalala I can't hear you I can't hear you lalalala..."***What Haskell is doing here is forcing you to write functions using IO within the IO Monad, which is a concept taken from mathematics and is actually very clever and useful once you learn more about the Haskell way of thinking. This is the only time in this tutorial where I'm going to use the word 'Monad'... except for the very very last word of the tutorial. ***OK, you can take your fingers out of your ears and stop chanting now. You can thank me later by email for protecting you from a tragic downward spiral in your sanity that would be inevitable as you try to fully understand the meaning continued in the concepts mentioned in the aforementioned paragraph.***So once we've read in our list of people, we want to convert it from a string into a type [Person]- Remember, we defined the Person type earlier. As you can see, we assign the name people, which has the type Person, as indicated by the line with the double colon ::, and read it out of the text string people_text. Note that in this case, we assigned the name with just a regular 'ol equal sign: We could do this, since we're not doing any IO any more... we already did this "dirty work" earlier. Functions in Haskell that don't do IO are called purely functional. The last two lines are obvious- They just print a message on the screen indicating the length of the people variable.
Several things- First, we were able to decouple our IO from the rest of our code- This helps reduce programming errors and is a major feature of Haskell programming (Clearly, this'll become more important later on, when our code is a bit more complex...) Something else that's really "good" is that when we read our people from the file, we didn't have to tell Haskell what type of data we were reading: The read function in the program used the compiler to figure out, on it's own, that a Person is a list of integers, and that reading a list of people therefore constitutes reading a list of list of integers, which is what we put into people.txt. If we had put anything else in there, read would have noticed this and complained when we ran our program. (In OOP-speak, this basically meant we polymorphically dispatched based on the return value of a function- Try doing that with your favorite Java compiler sometime :-)For one thing, we defined our people in a let expression in the middle of our main function... We did this so that we can simplify this tutorial by just appending new bits of code to the bottom of our program as we move along... Most Haskellers would just add these definitions separately outside of the main function, but either way works just fine. |