My own experiences with Haskell have been... unfortunate (either I am too stupid or my brain has been damaged by imperative programming languages). But this is easily the funniest introduction to any piece of software I have ever come across.
A key problem that I only realized after having recently returned to Haskell after a long absence is that our expectations about learning new programming languages and/or platforms set us up for failure. For example, I generally eat new programming languages for breakfast (or bedtime reading, really).
But I've noticed that Haskell isn't like picking up the PL flavor-of-the-week. Instead, it feels much more like learning a new area of mathematics. It's common with a novel area of math to just have to bang your head on it for a while. If you hang on through that phase, the cognitive switch flips and then you wonder how it was ever difficult. So it goes with a language that feels much closer to math reified, and for which most of the patterns we're used to from our imperative platform experiences just don't apply.
One good piece of advice I've seen a few places is to work through the U. Penn CIS 194 lectures and homeworks[1]. The lecture notes refer to other good resources for reading, including chapters of http://learnyouahaskell.com/ and more. But the real winner is just taking the time to just work through the homework problems. It's annoying at times. "Hey, (don't/shouldn't?) I know this stuff already?" But you'll probably get stuck at places, and those places pinpoint precisely the missing foundation blocks you'll need to reason successfully about your Haskell programs. Stick it out, but don't be afraid to search the web for help if you get too stuck. We self-taught types don't get office hours to keep us pointed in the right direction.
I agree on the math part (which is the biggest problem for me, since I do not exactly have a strong math background). Haskell requires one to think in very different ways from C or similar languages (compared to the difference to Haskell, the structured (C) vs. OO (say, Python or Ruby) difference seems negligible. If one already knows C++, learning the basics of Java is trivial (the main problem becomes learning about the standard library plus third party libraries). If one knows C/C++ and a Unix shell, learning Perl is at least manageable (is that how you spell that word?).
Lisp fans like to refer to their language of choice as something akin to Alien technology, but again, compared to Haskell, Lisp feels much more "terrestrial" (Common Lisp and Emacs Lisp, at least, my knowledge of Scheme is rather limited).
Now that I think about it, the real problem for me seems to be monads. Without having /some/ kind of side effects, you cannot write an even remotely useful program, and I never really got them. I once had the feeling I was really close, but then the toy program I was working on blew up in my face, thereby telling me I had probably not been as close as I had thought.
Another thing about Haskell (and - as far as I can tell, OCaml) that is /very/ different from the mainstream crowd of languages is the type system. I have a feeling that an idiomatic Haskell program would make far heavier use of the type system to model your problem than even extreme examples of object-oriented design (and without becoming as confusing as the more class-hierarchy-happy OO programs I have seen).
(Let me stress that I really adore Haskell's type system. If there was a conventional structured programming language with a type system like Haskell's, I would probably be all over it, as would many other programmers, I guess. I have wondered on occasion if you could have one without the other (immutability and such).)
Maybe I'll retcon learning Haskell on my list of New Year's resolutions. In all fairness, understanding Lisp took me about three to four years of looking at it once or twice a year, and understanding how pointers work in C also took me a lot longer than I am comfortable admitting. ;-) I can be very persistent if I really want to know something.
I've certainly learned more Maths as a result of learning Haskell, but I don't think the amount of Maths is vastly more than other languagues; or rather, other languages hide the Maths, either on purpose or by accident.
For example, Haskell uses terms like "Monad", "Monoid", "Functor", etc. which are Mathematical ideas, but as far as the language is concerned they're just interfaces (APIs). OO APIs can get just as gnarly, so if someone's comfortable with design patterns like inversion-of-control containers, dependency-injection mocking frameworks, factory factories, etc. then functional programming ideas are "different" but not necessarily "harder". Like all fields, there's a progression of ideas from simplistic, to powerful, to brain-bending, which everyone can scale at their own pace.
> ...the real problem for me seems to be monads. Without having /some/ kind of side effects, you cannot write an even remotely useful program, and I never really got them.
Whilst that's technically true, all it takes is one side-effect to have a useful program. It's perfectly acceptable to ignore monads and side-effects in all of your code, then write a simple "main" definition which plugs them together. In fact, that's the encouraged way of programming in Haskell! If you're playing around in a commandline, like GHCi, then you don't even need a "main" function at all.
If you're really struggling, you can often copy/paste a "main" which works for you. A trivial one is "print foo", but even in more realistic examples it's possible. To read and write over stdio, you can write the whole thing with pure functions, treat your argument as stdin and your return value as stdout, then do:
main = interact myFunction
The only time monads are actually required is when you need to choose between different side-effects, depending on the result of a previous side-effect. For example, if you write to a file, and the filename depends on the contents of another file:
main = do name <- readFile "I_CONTAIN_THE_NAME"
writeFile name myString
When side-effects don't depend on each other, you can use simpler alternatives to Monad, eg. Applicative:
Or, when you only need one side-effect, you don't need any of those interfaces at all:
main = print "Hello world"
> I have a feeling that an idiomatic Haskell program would make far heavier use of the type system to model your problem than even extreme examples of object-oriented design
The way I see it, if you're learning Haskell and you're tempted to throw an exception, you're probably using the wrong type. Exceptions can be useful, and there are advanced Haskell users who make regular use of them, but nowhere near the extent that they're used outside Haskell. Many "exceptional circumstances" simply never arise if you use stronger types.
progman posted some useful monad links, and I should know better than to write Yet Another Monad Explanation in a comment. But since you're coming from a C++ background, I can maybe fine-tune this to you personally. If it doesn't make sense, look somewhere else. :-)
First: There are actually two questions hiding here: (1) why are monads interesting in general, and (2) why does Haskell use monads to represent side effects? I'm going to mostly focus on (1) first, because that helped me more. Other people may strongly prefer starting from (2). My explanation will work best for people who like to make a model; explanations that start with (2) will work best for people who like to start with practical examples.
First, let's start with a C++ template:
template <typename T> List { ... }
Here, we have a list which can contain values of type T. So List<string> is a list of strings, and List<int> is a list of integers. Now, any Lisp or Ruby hacker would tell you that lists are better with a 'map' function. 'map' is a function which takes a list and a transformation function, and builds a new list by applying the transformation to every element of the old list. This way, if you have a list of strings, and a function 'convert_string_to_integer', you can write:
...and you've got a list of converted integers. If you understand this, you're over half of the way to monads!
To get a monad, we need to add two more things. First, we need a way to create a new list out of a single element:
List<int> new_list = List::from_element(3); // Creates the list { 3 }
And then we need the interesting bit. Imagine that we have a list of lists of integers, of type List<List<int>>:
{ { 1, 2, 3 }, {}, { 4 }, { 5, 6 } }
We want to flatten this into a single List<int> by smooshing all the lists together:
{ 1, 2, 3, 4, 5, 6 }
This function might be called 'join' or 'flatten' or (in Haskell), you might combine 'map' and 'join' into a "map a function then join the result" function called 'bind'.
A monad is basically any templated type that supports 'map' and 'from_elem' and 'flatten'. (There are a few common sense restrictions about how these functions related to each other, too.)
Lots and lots of interesting types either fit this model well, or can be smooshed into fitting. This model covers Python's list comprehensions, and JavaScript's promises, and Rust's Option type, and 90+% of collection types, and many much weirder and cooler things.
...and this brings us to I/O and Haskell. And here's where my explanation won't go far enough, but it might "click" for you latter on. In Haskell, conceptually speaking, you don't really perform I/O. Instead, you construct instances of "IO a", which is equivalent to the hypothetical C++:
template <typename T> IoAction { ... }
...where IoAction does some I/O and returns a value of type T. Then once you've described an I/O action, you ask Haskell to perform it. The type I/O action supports all our monad functions: If you have an IoAction<string>, it's easy to turn it into an IoAction<int> by running the original I/O action and applying a function to the output. Similarly, you can define IoAction::from_elem to (1) do no actual I/O, and (2) just return the elem you gave it. And if somebody gives you an IoAction<IoAction<string>> (an IoAction that returns another IoAction), you can "flatten" it by running the outer IoAction, taking the IoAction it returns, and running that.
So if you have an action 'chooseFileName' of type IoAction<string> and function 'readFile' which takes a string as an argument and returns an IoAction<string> that reads in the specified file, you could write:
chooseFileName.map(readFile)
...and get an IoAction<IoAction<string>>, and if you flatten it:
chooseFileName.map(readFile).flatten()
...you get a new IoAction<string> that prompts for a filename and reads a file. Or you could write:
...and get an IoAction<string> that returns whatever is in "my_file.txt". And this is how Haskell I/O works. But you'll probably have to play with it for a while until all the pieces fit together; Haskell is mathy that way.
Hehe, I just took a look and found this very encouraging statement: "Slow learners are often the most thorough learners, this is something to celebrate!"
I'm finding it the first useful introduction to the language I've encountered. Like you, my experiences have been less than exemplary, and I'm way too arrogant to accept that I'm either stupid or brain damaged. Every year or so I take a run at Haskell, and this time I think I'm going to get it. I know a dozen-odd languages, including a fairly expert grasp of APL and Mathematica, both of which are a bit odd in their approach to life. Haskell is genuinely weirder than most, and the entire Haskell community's apparent total ignorance of the of the most rudimentary principles of pedagogy make the learning curve unreasonably steep.
The fact that most Haskell tutorials start out with contradictions is an example of this. Pedagogy 101: don't make false claims about the subject in terms of the concepts you can reasonably expect the student to have.
For example, electrical engineers should never introduce the concept of "negative resistance" by starting off talking about "negative resistance". They should first introduce the concept of generalized resistance (dV/dI rather than V/I) and then show how generalized resistance can be negative. To tell a student that there is such a thing as "negative resistance" when you know full well that "resistance" in the student's mind is not generalized but defined by a relation such that it cannot possibly be negative is simply incompetence on the part of the teacher.
By the same token, it doesn't matter one whit that "the language" is side-effect free when "the runtime" implements side effects when the student can reasonably expected to understand "language" to mean "language plus runtime" as it is in every other case. Actually: it does matter, and it should be explained in those terms. Otherwise students will rightly reject the language as some kind of snake-oil gibberish promoted by arrogant hucksters.
No Haskell tutorial should ever make the false claim that "the language" (as understood by any reasonable student) is "purely functional". Students aren't stupid and haven't been brain-damaged by imperative languages. They know that "the language" covers the whole package in ordinary parlance, including the runtime. When we talk about Perl or Python or C++ or FORTRAN that's what we mean. There is absolutely no basis for anyone to ever assume any other understanding on the part of the student.
So tell people "Haskell is implemented in such a way that it can be treated as a purely functional language for the purpose of static analysis even though at runtime there are various clever tricks that still allow it to implement side-effects like IO". That is a truthful statement. "Haskell is a pure functional language and for every function call the return value depends only on its arguments" is false, outside of a tortured and bizarre interpretation of the terminology that has no apparent purpose but to misrepresent reality to outsiders.
There are some pretty cool things about Haskell. The type inference system is powerful, useful and interesting. It's remarkable to see how far a nominally (but not actually) purely functional language can be pushed, and the way various hacks are used to overcome the limitations of purely functional languages is incredibly clever. The creators of Haskell should be celebrating those feats, not hiding behind false claims that the language+runtime is "truly" purely functional when it manifestly is not.
Edit: Concrete example: to be a purely functional language function values must depend only on their arguments. Here is an example of a Haskell function whose value does not depend only on its arguments:
notPurelyFunctional :: Double -> IO Double
notPurelyFunctional x = do
z <- putStrLn "Please enter a number: "
name <- getLine
let y = read name ::Double
return (x+y)
we have no idea what "value1 == value2" will evaluate to, so either:
a) The above code is not part of the Haskell language (which would be weird as it all runs just fine under GHC, and "stuff that runs under GHC" is what any ordinary person means by "Haskell")
or
b) Haskell is not a "pure functional language" the way that term is ordinarily understood, which is: "calling the same function multiple times with the same arguments will always produce the same results" (in which case "value1 == value2" must evaluate to "True", which it does not in general when running the above code).
Q: Is it true that the return value of any Haskell function depends only on its arguments?
A: Yes, barring tricks like unsafePerformIO. But you can write perfectly good programs that do IO without these tricks anyway.
Q: Okay, genius. What about functions like getLine, which reads a line from the console?
A: getLine isn't a function.
Q: What?
A: All functions in Haskell have types like "a -> b" for some a and b. But you'll notice that the type of getLine doesn't have an arrow in it, it's just "IO String".
Q What do you mean, it's not a function? I can call it right here, by doing "do name <- getLine"!
A: That's not the syntax for calling functions in Haskell, that's monadic do notation. The syntax for calling functions is "foo x y".
Q: This is outrageous. So you're saying that getLine is just an inert value?
A: Yes.
Q: But what is that value? What's inside the type "IO String"? I'd always assumed that it was a kind of wrapper around a String, with some type system nastiness so people don't misuse it.
A: No, I'm afraid it can't be anything as simple as that. And it certainly can't contain a String, because then getLine couldn't be the same value all the time.
Q: Wait, you're saying that it doesn't even contain a String? Then what is it, really?
A: The implementation of IO types is kind of private to the runtime. But you can imagine that for any Haskell type X, "IO X" is a syntax tree of a side-effecting C program that produces an X.
Q: I see. So getLine really is the same value all the time. But how do you "run" the syntax tree that's hidden inside getLine, to actually get a String?
A: You generally don't. Instead you combine them together into one large tree and call it "main", and the runtime takes it from there.
Q: That seems like a lot of work. At the very least, why doesn't the language add some special syntax for combining syntax trees, so people don't have to deal with plumbing?
A: Yeah, that's what monadic do notation was invented for. Unfortunately, it was a bit too successful and some people started confusing it with actual imperative code :-)
"and then the runtime takes it from there" is of course the right answer, but no one upon first encountering Haskell understands the distinction between "the language" and "the runtime", so all the insistence that "the language" is "purely functional" is utterly misleading.
Furthermore, the claim that "getLine is really the same value all the time" is false. If that were the case, then any operation on getLine would return the same value, which it does not.
Edit: And in any case, the question is not whether "getLine" is a function, but whether "notpurelyFunctional" is. It has the signature of a function, but its return value certainly doesn't depend only on its arguments, or we would be able to say if value1 == value2 was true or not, and we can't.
Basically, any claim with the word "really" in it evaluates to false (in philosophy claims about what is "really" or "really really" true are sometimes enhanced by what is known as "a certain percussive emphasis.")
I think monads are an incredibly clever way of working side-effects into a language while maintaining an interesting level of functional purity. But the way the concepts are presented are just not conducive to conveying them to newbs, who will rightly call bullshit based on what words like "the language" are normally construed to mean.
> but its return value certainly doesn't depend only on its arguments
Note that value1 and value2 are not, and cannot possibly be, return values of notPurelyFunctional. The type of notPurelyFunctional says its return value has type IO Double, but value1 and value2 have type Double. IO Double is not Double, doesn't contain a Double, and cannot be converted to Double by a function call.
It sounds like you still think that the syntax "do x <- foo" means something like "let x be the result of some function call involving foo". Actually it can't be anything like that, it translates to something more compicated.
> If that were the case, then any operation on getLine would return the same value
Yes, and moreover, any operation on notPurelyFunctional 5.0 always returns the same value.
You have my honest word that I have some experience with Haskell and believe these things to be 100% true, with no wishy-washy philosophical caveats. I know exactly how to write an interpreter in which the machine representation of something like "notPurelyFunctional 5.0" will be immutable bit-for-bit, without using any dirty tricks like pointers to mutable memory. And I know that it's immutable in actual Haskell implementations as well.
You misunderstand. IO is just as much a purely functional concept as the rest of the language. notPurelyFunctional is a purely functional function. "notPurelyFunctional 5.0" will behave the same way no matter what you do with it.
You cannot be sure what value1 == value2 will evaluate to even if you assume all the functions (including notPurelyFunctional and (>>=)) are pure(which they are)
One way to make sense of this is to think of 'IO a' as instructions for the computer to compute a value of type a. So you can think of your notPurelyFunctional function as taking a Double, and returning instructions for the computer to compute another Double. notPurelyFunctional 5.0 would then create a value with something like the following information.
let z(which will always be ()) be the result of printing the string "Please enter a number: "
let name be the result of reading a line from stdin
let y be name parsed as a Double
compute and return the value (5.0 + y)
Notice that no matter when or how many times you call notPurelyFunctional with 5.0, the instructions generated will always be exactly the same. When you run your program, the value called main in your program will first be evalutated purely to produce these instructions, and only once these instructions have been (purely) generated will your program be run and these instructions be executed. Thus Haskells purity is preserved at every stage.
Note that this was a very crude way of dealing with how IO behaves and is only meant to be an aid for grasping the concept of IO.
The unfortunate part with regard to your wish is that Dr. McBride is a professional punmaker who was poached by Strathclyde for his side hobby in theoretical computer science.
AFAICT, this (the punmaking, and language games in general) is actually a prime attribute of PL researchers, at least in several major PL subdisciplines. It's been awhile since I've cracked open the POPL proceedings or the like, but ... wow, just wow.
One of my favorites was a (peer-reviewed, published!) paper with this gem of a footnote in the first column: This work supported in part by cinder blocks.
I am not sure if this applies, but in my experience, presenting a dry, more or less theoretical subject matter in a humorous way makes it substantially easier for me to stay focused.
Also, as somebody else already said, if you are designing languages professionally, you probably get to appreciate puns on a whole new level.
What's wrong with puns? It's not in bad taste. And the rest of the stuff is pretty funny as well ("At this stage in its life cycle, it's unlikely to be particularly robust. You'll find that its error support is fantastic, if you need help making errors."). At the same time, the humour doesn't get too distracting. Much nicer to read than some four-letter-words punctuated piece full of hyperbole.