Let's first distinguish between learning the abstract concepts and learning specific examples of them.
You're not going to get very far ignoring all the specific examples, for the simple reason that they're utterly ubiquitous. In fact, the abstractions exist in large part because they unify the things you would be doing anyway with the specific examples.
The abstractions themselves, on the other hand, are certainly useful, but they're not immediately necessary. You can get pretty far ignoring the abstractions entirely and just using the various types directly. You'll want to understand them eventually, but you can always come back to it later. In fact, I can almost guarantee that if you do that, when you do get back to it you'll slap your forehead and wonder why you spent all that time doing things the hard way instead of using the convenient general-purpose tools.
Maybe a as an example. It's just a data type:
data Maybe a = Just a | Nothing
It's all but self-documenting; it's an optional value. Either you have "just" something of type
a, or you have nothing. Say you have a lookup function of some sort, that returns
Maybe String to represent looking up a
String value that may not be present. So you pattern match on the value to see which it is:
case lookupFunc key of
Just val -> ...
Nothing -> ...
Really, there's nothing else you need. No
Monads or anything else. Those express common ways of using
Maybe a values... but they're just idioms, "design patterns", whatever you might want to call it.
The one place you really can't avoid it completely is with
IO, but that's a mysterious black box anyway, so it's not worth trying to understand what it means as a
Monad or whatever.
In fact, here's a cheat sheet for all you really need to know about
IO for now:
If something has a type
IO a, that means it's a procedure that does something and spits out an
When you have a block of code using the
do notation, writing something like this:
do -- ...
inp <- getLine
...means to execute the procedure to the right of the
<-, and assign the result to the name on the left.
Whereas if you have something like this:
do -- ...
let x = [foo, bar]
...it means to assign the value of the plain expression (not a procedure) to the right of the
= to the name on the left.
If you put something there without assigning a value, like this:
do putStrLn "blah blah, fishcakes"
...it means to execute a procedure and ignore anything it returns. Some procedures have the type
() type is sort of a placeholder that doesn't say anything, so that just means the procedure does something and doesn't return a value. Sort of like a
void function in other languages.
Executing the same procedure more than once can give different results; that's kind of the idea. This is why there's no way to "remove" the
IO from a value, because something in
IO isn't a value, it's a procedure to get a value.
The last line in a
do block must be a plain procedure with no assignment, where the return value of that procedure becomes the return value for the entire block. If you want the return value to use some value already assigned, the
return function takes a plain value and gives you a no-op procedure that returns that value.
Other than that, there's nothing special about
IO; these procedures are actually plain values themselves, and you can pass them around and combine them in different ways. It's only when they're executed in a
do block called somewhere by
main that they do anything.
So, in something like this utterly boring, stereotypical example program:
hello = do putStrLn "What's your name?"
name <- getLine
let msg = "Hi, " ++ name ++ "!"
...you can read it off just like an imperative program. We're defining a procedure named
hello. When executed, first it executes a procedure to print a message asking your name; next it executes a procedure that reads a line of input, and assigns the result to
name; then it assigns an expression to the name
msg; then it prints the message; then it returns the user's name as the result of the whole block. Since
name is a
String, this means that
hello is procedure that returns a
String, so it has type
IO String. And now you can execute this procedure elsewhere, just like it executes
Pfff, monads. Who needs 'em?