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!"
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.
[1] This term in particular gets referenced: http://www.seas.upenn.edu/~cis194/spring13/lectures.html