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.
Take 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 -> ...
That's all!
Really, there's nothing else you need. No Functor
s or Monad
s 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 a
value.
When you have a block of code using the do
notation, writing something like this:
do -- ...
inp <- getLine
-- etc...
...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]
-- etc...
...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 IO ()
--the ()
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 ++ "!"
putStrLn msg
return 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 getLine
.
Pfff, monads. Who needs 'em?