I've been writing Haskell code for years, and this is one of the most singularly useful articles I've read on it. I've certainly found the proliferation of "monad tutorials" and "learn this so you can print something" guides obnoxious, but it never occurred to me that IO really could be taught completely separately from monads.
Here's the outline of what the middle of a tutorial could look like, following this advice:
Here's how to print a string (putStr, putStrLn). Here's how to print something other than a string (show, print). Here's how to print two somethings in a row (>>). Here's how to read something and print that (>>=). Here's how to print something in a function other than main (:: IO ()). Here's how to read something in a function other than main (:: IO a, >>= again, return). Oh by the way, it turns out that pattern (return, >>=) is so common that Haskell gives it a special syntax (do notation). For instance, it works with the previously introduced Maybe. And with List. Remember typeclasses? You can write functions that work with IO, Maybe, and List (Monad m => ...).
Even when actually talking about monads, the explanation should come first, and the term should come later. "It turns out these operations are a common enough pattern that Haskell gives them special syntax..." comes across much more sanely than "now we're going to introduce something [complicated] called a monad".
I think your suggestion is a relative improvement over most tutorials. But it's still unmanageably hard.
"Here's how to read something and then print that: >>=". Sure, like `getLine >>= putStrLn`. Technically speaking, that's correct. But this doesn't generalize at all.
Maybe I want to greet the user by name. How do I read the name, prepend "Hello, " to it, and then print that? This is a totally natural thing to want to do, and is very straightforward in Java. But our Haskell newbie hits a wall - none of your cases cover it!
Here's how I might write that today, without using do notation: `getLine >>= (return . (++) "Hello, ") >>= putStrLn`. Look at that - it's totally nuts, and I would not attempt to explain that to a Haskell neophyte.
Using do-notation, you can make things nicer:
do
name <- getLine
putStrLn $ "Hello, " ++ name
but this introduces a bunch of new syntax to learn: the difference between let-in and let and <-, how do-notation works, etc.
I don't have a better approach than what you suggest. I suspect that this stuff simply can't be made easy.
This is a nice approach. Thanks to all of you in this thread. I think I'll try something like this when I introduce Haskell I/O in my Programming Languages class in a month or two.
If this is meant to be under main, you need a "let":
main = do
let greet name = putStrLn ("Hello, " ++ name)
getLine >>= greet
For whatever reason, I didn't learn about the magic let syntax in do-notation until relatively late. Anyways this is very elegant, but not something I could have generated early on, especially because it mixes two different sequencing syntaxes (do-notation and >>=).
You can use "fmap" to make this nicer (but that introduces yet another concept). If you use hlint on the program above, you it will tell you to use fmap instead.
fmap ("Hello, " ++) getLine >>= putStrLn
I learned this because hlint suggested it to me. Using hlint is a very good idea, even for beginners.
For a simple "greet" program, this isn't perhaps any neater or cleaner but fmap is a powerful tool with lots of practical uses.
There's one thing that should be mentioned here: you can easily compose these things. You can do:
echo = forever $ getLine >>= putStrLn
This also works with do notation, if you'd want to make the greet example looping.
greetLoop = forever $ do
name <- getLine
putStrLn ("Hello, " ++ name)
This is a very powerful concept. In mainstream imperative programming languages, you can't assign "statements" to variables or return them from functions or pass them as parameters.
Well, in this simple example it is trivial to do the same by wrapping statements in a closure, but it's a bit more nuanced than that. But you still can't assign a for-loop as a value to a variable, for example (to exaggerate a little).
This becomes more apparent when using monads for non-IO applications like parsing. It's difficult (but not impossible, of course) to implement things like backtracking or non-determinism by writing lots of closures in an imperative programming language, they don't compose as nicely. There are parser combinator libraries (similar to Parsec) done in other languages of course, but some of them even implement monads in the language they work in. I recall seeing "class Monad" written in Python, I think it was in the pyparsec library.
A pointfree solution - if you want one - would be more in the form of:
getLine >>= putStrLn . ("Hello, " ++)
It's really mostly about getting used reading pointfree style, but
there's certainly an overuse of pointfree style in some Haskell code,
then it gets pointless.
That's a very contrived example you obviously didn't spend any time trying to understand. No-one in their right mind would write code like that, except to demonstrate something.
And if you did write code like that, Haskell's hlint tool would make suggestions to turn it neat and readable using fmap.
Why wouldn't I want to write that? Clearly you need a little practice before instantly recognizing a partially applied function where the next parameter will be appended to your other string, but that one-liner has actually a very nice computing flow.
That's a bit hard to read, isn't it? (return . f) is a common idiom (or antipattern?) which is better solved by using fmap. hlint will tell you this. For example:
fmap ("Hello, " ++) getLine >>= putStrLn
This isn't necessarily better either, I'd write this using do-notation.
I understood every bit of that code, I just think it's ugly and hard to read and there are better ways to get the same done.
My take on how to explain basic IO without having to explain monads:
(1) Any function with interacts with the world is called an IO function and has the result type "IO something". e.g. "IO String" for getLine (because it returns a String). Functions and expressions which does not interact with the world are called 'pure'. IO is a type constructor, but for now it makes sense to think of it as a "tag" on result values which guarantees we cannot mix IO-functions with pure functions. If we could call IO functions inside pure functions, the whole idea of pure would dissolve. The IO "tag" allows the compiler to enforce that this can't happen.
(2) A function which calls IO functions uses 'do'-notation:
somefun = do
putStrLn "Hello"
putStrLn "World"
Each line is a single IO function call (with arguments which are pure expressions). Each line in the do-block should be indented to the same level. (Only in the case where the function consist of just a single IO-call can the 'do' be left out, which is why you can write 'main = putStrLn "hello World"'). The do-notation guarantees that the operations are executed sequentially. The return value of the function is the return value of the last operation in the do-block.
If you want to use the return value of an IO function, you use a left-arrow (<-) to read it into a variable. Eg.
somefun = do
putStrLn "What is your name?"
name <- getLine
putStrLn ("Hello, " ++ name)
Note that you cannot simplify the two lines to:
X putStrLn ("Hello, " ++ getLine)
The above breaks the rule that each IO function call have to be on a separate line in the do-block. IO-function calls can not be nested in expressions, they have to be sequential.
(3) Calling pure functions.
You cannot call IO function from pure code, but you can call pure functions from IO-code. This is done via 'let':
somefun = do
putStrLn "What is your name?"
name <- getLine
let name2 = reverse name
putStrLn ("Olleh, , " ++ name2)
Note how the let syntax is distinctly different from the left-arrow syntax. The let syntax just assigns the result of a pure expression to a variable. The arrow syntax "extracts" a pure value from an IO value. The result of getLine is 'IO string' but the arrow "untags" it so the type of name is just "String", and it can be used as part of a pure expression.
(4) Returning pure values.
The return type of a IO function has to be IO something. In the above example the return type is 'IO ()' since this is the result type of the last operation, putStrLn. But what if we want to return say the reversed name? The reversed name is a pure value, so this is will not compile:
somefun = do
putStrLn "What is your name?"
name <- getLine
let name2 = reverse name
putStrLn ("Olleh, " ++ name2)
X name2
We have to turn the String into an "IO String", because the result type of an IO function has to be "IO something". This is done with the "return" operation, which turns a pure value into the corresponding IO value.
The name "return" is unfortunate since it looks equivalent to "return" in imperative languages. But in Haskell "return" doesn't actually exit the function. The function exits after the last operation regardless of the use of "return". The only thing "return" does is to "tag" a pure value so we can return it from an IO function:
somefun = do
putStrLn "What is your name?"
name <- getLine
let name2 = reverse name
putStrLn ("Olleh, " ++ name2)
return name2
I read the article, and it annoyed me, because the rebuttal is obvious and wasn't addressed.
The rebuttal is do-notation. The second program you write after Hello World is going to use two IO actions instead of one, and so you need a way to sequence them, and every tutorial is going to do that with do-notation. And suddenly all of the other syntax you learned, like how to declare a variable with let-in, or how arrows go -> that way, or how to call a function, is tossed out the window. And you'll spend ten minutes before you realize your indentation is wrong and then ten more fighting with how you declare a variable and then apply a function to it before it dawns on you, this is monads.
You don't have to use do-notation, sure. You can use >>= as an infix operator. It's still totally confusing.
The reality is that monads hit you hard and fast as soon as you want to do any IO. Not the monad laws or functors or whatever the tutorials focus on, but the nitty-gritty syntax. This is where I got stuck and gave up (twice), before I finally got it.
Yeah, no way around learning do-notation even for the simplest of programs. And do-notation is kind of a DSL with a totally different feel than the rest of the language. So getting started writing even simple toy programs in Haskell requires you to learn two languages. No denying that Haskell is a language with a very steep learning curve. (Or is it a very shallow learning curve? Never understood that metaphor. But you have to learn a lot to get started.)
However it might make sense in a tutorial or course to teach do-notation first as a DSL for IO in order to get started. And only later, when the student have a good understanding of the core language and type system, introduce monads and show how IO is a monad and how do-notation can be used with other monads.
The problem with the much maligned monad tutorials is that the audience typically is beginners who get stuck on on the IO monad and do-notation immediately after writing "hello world". But the tutorials try to explain monads as a general concept, which requires a pretty through understanding of things like higher kinded types and so on. An understanding the newbie getting stuck at "hello world" obviously doesn't have yet. Then the tutorials try to be accessible by using metaphors like a "monad is a specesuit", a "monad is burrito" etc. But knowing that a monad is a buritto doesn't help the newbie who gets inscrutable compile errors after adding a second putStrLn to "hello world".
In operations management, a "learning curve" is an efficiency curve plotted over time. I have most often seen it as a time-per-task-completed curve. The way it works is this:
The time it takes to produce widget 2 ^ t is BASE_TIME * LEARNING_RATE ^ t.
Example with 120 minute starting time and 90% learning rate:
If you plotted a curve of minutes per widget over time, you would note that the graph for the 70% learning rate would have a much steeper slope than that of the 90% learning rate.
Looking at that you might say, "ah, high learning rates are great!" All else being equal, you would be correct. However, real world tasks with high learning rates often imply high base times and a proportionately large amount of time until proficiency or mastery is reached. That's why "steep learning curves" are "bad things", because it is going to take "a lot of effort" just to "get good" at the skill.
To bring the point home, here are some contrived examples of "90%" and "70%" skills:
Version control:
90% - copying and pasting files
70% - git
Word processing:
90% - Microsoft Word
70% - vim and latex
Databases:
90% - Excel documents in a shared folder
70% - PostgreSQL
Web design:
90% - your web host's website builder
70% - Ruby on Rails
70% - hand coding in notepad
This is just an introduction to the concept as I learned it. Two skills might have the same learning rate, but one might be much more productive than the other. Still this should help you understand the origin of "steep learning curve" and why that's usually considered a "bad thing."
The issue isn't as great as you portray it to be. You're facing beginner issues and learning a foreign topic, expect to spend some time on it and put in the mental effort and you'll master it in no time. After spending an hour to write "Guess the number" game with and without do notation, you should know enough to be able to write practical programs in Haskell. Yes, an hour for guess the number is a lot, but it's the learning part that takes the time, not just putting a program together.
It makes perfect sense to have do notation and desugaring it to use >>=. For "imperative" programming, do notation makes sense. Other times you'll be writing data processing "pipelines" where using >>= is more convenient. To give an example:
readQueries inFile >>= readFromDb db >>= writeOutput outFile
The above would be very clumsy to write in do notation (but not too difficult, that's what you'd do in an imperative language). But writing "guess the number" without do notation is also clumsy (but a good learning exercise).
do notation and monad laws may be a foreign concept when coming from an imperative programming background, but it's also a neat an elegant concept that works very well in practice.
I have mastered it, but I have not drunk so much Kool-Aid that I have forgotten what it took!
It sure did not take "no time," and much longer than an hour - longer than any other programming language I have attempted. The biggest stumbling blocks were syntactical. For example, there are three ways to declare a variable (let-in, let, and <-) and if you use the wrong one, you get a very unhelpful error message. The learning was not so much new concepts but instead things like how a "not in scope" error means you used a tab instead of spaces.
I completely agree, the underlying concepts are elegant, and powerful and enlightening and quite amazing. But you can understand the concepts perfectly and still not be able to write a line of Haskell. That's where most tutorials fall short.
Perhaps you interpreted me a bit too literally. Of course it takes time for internalizing new and foreign concepts. The lecture, lesson or exercise might only take an hour but it may take days or weeks to grok the concepts. And this might mean a lot of practice.
I certainly remember having a hard time understanding this years ago, and my problem was probably the same as yours - the lack of nice books and tutorials (but things are a bit better now) as well as it all being new and foreign. I don't regret putting in the effort, though.
I also do not like the inconsistency in let-in vs. let (do notation), but I guess there's a good reason for that. <- is not strictly "variable assignment" but it seems superficially similar, though (if comparing to imperative languages).
But I don't think these issues can be "fixed" without turning Haskell into something completely different (e.g. something more like ML). Or do you have a good suggestion how to simplify this without making it worse?
He actually does go into that in the "Some side notes" part. He recommends calling it a monad only when you actually use the monadic interface:
“I have to directly use the result of one IO action in order to decide which IO action should happen next”: Yes, this is a use case for IO’s monadic interface."
Then yes - you need the do-notation.
For simple sequencing he recommends to use it's Applicative instance.
Your overall point is correct though - choosing to avoid the do notation in Haskell will likely limit your ability to read and understand a lot of code just because monads are so commonplace in Haskell.
yea, but the struggle isn't really quite with monads, but with syntax. properly understanding the syntax for do notation with use in IO does not require an understanding of monads.
They pretty much are as without sequencing you can only do the most trivial thing (a single IO action). Most interesting programmes are going to need at least two IO actions.
Here's how I like to explain Monads to people. I've been told it's a decent explanation. (It ignores the monad laws and such, but it's a decent conceptual overview.)
OK, so you know Java interfaces? Haskell has something just like that. They're called "type classes", though. But it works pretty much the same way. When you write out a class definition, you write A) the name of the class B) all the functions the class has to support and C) the types of those functions. (Like a Java interface definition.)
Haskell has a type class called "Monad". To "implement the monad interface", in Java parlance, you have to implement two functions. The first is called "bind", but it's written ">>=". Here's its definition:
>>= :: m a -> (a -> m b) -> m b
Let's say our Monad was called `Foo`. In Java-land, this means the function takes a `Foo<a>`, a function that takes an `a` and returns a `Foo<b>`, and it uses those two things to return a `Foo<b>`.
The other function is "return", which is defined as
return :: a -> m a
Which, in Java terminology, means it takes an `a` and returns a `Foo<a>`.
If you implement those two functions, you now have a Monad.
If you've been told that Monads do anything in particular, you've been told wrong. Monads don't necessarily have anything to do with IO, or sequencing, or state, or anything like that.
It just so happens that those two functions (">>=" and "return") are super useful for representing a whole lot of common things, which happen to include things like IO and stateful algorithms. Monads don't have to do either of those things, but they certainly can.
I see that you have invoked ire with your comment but I have to say:
1. I have never even attempted to grok Haskell
2. I read the article, noted that the author never actually defined 'monad' even those he raised the question, and asked myself 'yes, but what is a monad?'
3. I was going to Google it, but decided to check these comments for some concise answer so that I might avoid some kind of Haskell rabit hole when I should be coding.
I found your comment to be just what I was looking for and written quite concisely. So, thanks, and haters gonna hate.
Every explanation of every Haskell feature I encounter reads the same way as this. You define "return" as "<code>return :: a -> m a</code>" and say this means it takes "an a" (by which I assume you mean an object of type a?) and returns a "<code>Foo<a></code>" (an object of type <code>Foo<a></code>?).
The problem I have is: what on earth does this have to do with 'm'?
Is 'm' equivalent to 'Foo'?
If so, why don't you mention that? It isn't obvious to a newb who is lost in the welter of syntax.
Does 'm a' mean the same thing as <code>Foo<a></code>? If so, why not use 'Foo' in your Haskell rather than 'm', which is simply introduced without any description, mention or explanation? Is there some significance to the naming? This is the kind of question that a newb specifically cannot answer, and Haskell tutorials are notoriously lax about. They have a strong tendency to assume you know the syntax of the language, which they use to explain the syntax of the language.
You've given a nice, detailed explanation, but have failed to describe what 'm' is, and that means your average newb (me, or possibly even someone much, much smarter than me, as I'm told such people exist in profusion) is left making guesses. I'm pretty sure I know what the correct guesses are, but I really want to know: why did you use a completely different, unrelated, and opaque name for 'Foo' in your Haskell example, and then talk exclusively about 'Foo' in your explanation?
This isn't really a knock at you personally: as I said, every Haskell tutorial I've encountered has similar issues. I'm just deeply curious as to why this is the case (and painfully aware that my own explanations regarding other languages are likely as opaque to newbs as these Haskell explanations are to me.)
The fundamental problem is higher order types. When you define an interface in Java or C#, it has no way of referring to itself. Something like "compareTo" cannot say "the other type must be exactly equal to this". So lets invent such a syntax (using a keyword "as") and write the Monad interface:
interface Monad as m {
const m<b> bind<a,b>(m<a>, Function<a,m<b>>);
const m<a> return<a>(a);
}
If you fill in the implementing class for m (like IO or List or Maybe), you get the actual type signatures that would maybe be valid in C# or something.
The 'm' is a type variable. This is a consequence of a difference in how Java interfaces and Haskell typeclasses are defined. (I'm continuing wyager's analogy between Java interfaces and Haskell typeclasses here.)
In type signatures, an identifier starting with a lowercase letter is a type variable. So 'p' is a type variable here, like the 'm' type variable used in the Monad typeclass. It refers to the type which, in Java terms, implements Pushable. (In Haskell you'd say the type "is an instance of Pushable".)
So why this difference? Java, being a C++-style OO language, privileges the implicit first argument. When class Button implements Pushable, you can think of its push method as having a real signature of "void push(Button this, bool shouldShove)". This particular pattern is special in Java, so you don't have to write the "Button this" parameter explicitly. But it's not a special pattern in Haskell, so you do need to explicitly write the 'p' in "push :: p -> Bool -> ()".
To drive the point home, maybe one more comparison is useful. In Java, when an interface method needs to take a parameter that has the type of the implementing class, you need to do something that looks a bit more like Haskell. Here's an example:
You're absolutely right; that is confusing. Swift's explanation in response to your comment is good, so hopefully that helps.
There are a few reasons I wrote "m a".
When you use a lowercase letter in a type definition (like "m" or "a"), that's a type variable. The type isn't anything in particular. In a function like
id :: a -> a
that "a" can become anything when you use the function later. So you can use "id" on integers, or floats, or strings, or whatever. And with functions like
add3numbers :: Num a => a -> a -> a -> a
that "a" can become anything that's an instance of the "Num" typeclass (in Java: anything that implements the "Num" interface). "Num" is a restriction on the value of "a", but "a" is still variable.
If I were to write a function that had an uppercase word in the type, like
doFooThing :: Foo -> Foo
an uppercase type indicates that this isn't a type variable, but rather a specific type. In this case, the function only takes and returns the type called "Foo". So that's why I used a lowercase word in my definition of ">>=", as it works on any Monad, not just Foo. Here's how you actually write out the definition of the Monad typeclass:
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
In the definition of >>=, we don't define it in terms of Foo or any other specific Monad; we define it in terms of a type variable (which is idiomatically "m", for "Monad"). "m" can then be filled in with any Monad (like Foo, or Maybe, or IO) when the time comes to actually use ">>=".
As you can see, this is not as nice as e.g. "a -> m a"
So it would have been more correct for me to have said "here's the type signature for >>=, and when you use >>= with something called Foo that implements the Monad interface, it <explain >>='s type in terms of Foo>."
Can you add some examples of how bind and return get defined for commonly used monads, like IO? I'd love to have a more intuitive grasp of what they're for.
IO is a bad example; its instance of Monad normally exists in native non-Haskell code (or in Haskell-runtime-specific code) rather than in Haskell. Another good reason that talking about the "IO monad" early on doesn't work out well. So, I'll start with a couple of non-IO examples and then provide a suggestion for how IO works.
Let's start with Maybe. The Maybe type looks like this:
data Maybe a = Just a | Nothing
You often use Maybe to represent functions that only handle some inputs:
safeHead :: [a] -> Maybe a
safeHead (x:_) = Just x
safeHead _ = Nothing
> safeHead [1,2,3]
Just 1
> safeHead []
Nothing
The Maybe monad lets you assemble a series of operations of type Maybe into a single compound function of type Maybe. If any of the individual operations produce Nothing, the whole function produces Nothing.
safeAddHeads :: Num a => [a] -> [a] -> Maybe a
safeAddHeads l1 l2 = do
v1 <- safeHead l1
v2 <- safeHead l2
return v1 + v2
> safeAddHeads [1,2,3] [2,4,6]
Just 3
> safeAddHeads [1,2,3] []
Nothing
That do notation get translated to the following uses of bind and return:
Bind (>>=) implements this behavior, of obtaining a value and feeding it into the next combination, or returning Nothing:
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
Just a >>= f = f a
Nothing >>= _ = Nothing
Return takes a plain value and makes it a monadic value, so it's just the "Just" constructor:
return :: a -> Maybe a
return = Just
(You could also write that as "return a = Just a", but you'll often see Haskell functions written in "point-free style", which means omitting explicit parameter names when not needed.)
So, that's bind and return for Maybe.
For a more unusual example, lists are a monad. The list monad acts like a cross-product: for every value in the list, do the remaining operations. For instance:
If that looks a lot like a list comprehension, that's not a coincidence.
Return, again, is easy:
return :: a -> [a]
return a = [a]
Bind implements the behavior of running the right-hand side for each item on the left and combining the results into a list:
(>>=) :: [a] -> (a -> [b]) -> [b]
(x:xs) >>= f = f x ++ (xs >>= f)
[] >>= _ = []
Note that the list bind operation calls itself recursively here: first call f on the first item, then call it on the rest. This could also be written with a fold or map; for instance:
xs >>= f = concatMap f xs
concatMap just runs concat on the result of map. The map applies f (of type a -> [b]) to each item in the list (type [a]); since f returns a list, that results in a list of lists ([[b]]), which concat combines into a single list ([b]).
You might want to take a look at the Writer, Reader, and State monads for further examples. Note in particular that those three maintain some internal state information inside the monad, and in addition to the bind and return operations, they offer operations that work with that internal state.
Having given those examples, here's a sketch of how IO could work. IO operations act as though they change some state (the outside world), and if you chain them together, the second operation acts on the state resulting from the first (for instance, if you call getLine twice, the first modifies the state of stdin by reading a line from it, and the second starts where the first left off by reading another line). That state isn't actually representable in Haskell, but let's imagine that it is.
So, the IO type (to focus on the terminology suggested by this article) contains a value along with an operation changing the state of the world:
data IO a = IOPrivate (StateOfTheWorld -> StateOfTheWorld) a
Notice that a value of IO type contains an operation changing the state of the world, rather than an actual state of the world. It helps to think of an IO type as representing an operation to be run, and main is the top-level operation that actually gets run when you invoke the program. Continuing that analogy, something outside your program, provided by the Haskell runtime, does this:
run :: IO () -> StateOfTheWorld
run (IOPrivate f ()) = f initialStateOfTheWorld
(Compare this to functions like runState, for instance.)
Return takes a non-IO value and makes it into an IO value that doesn't actually change the state of the world:
return :: a -> IO a
return a = IOPrivate id a
Bind sequences the changes to the state of the world:
(>>=) :: IO a -> (a -> IO b) -> IO b
(IOPrivate sf1 v1) >>= f =
let IOPrivate sf2 v2 = f v1
in IOPrivate (sf2 . sf1) v2
> Did you even understand that the whole point of the article was about not bringing up Monads? Jesus.
Indeed, but there were people in this thread who were asking about them, and I will always prioritize people who are asking to learn something over people who are asking not to teach something.
I've also experienced similar failures. I've tried to learn Haskell several times, and every time I meet the Monad section in the tutorial I lost my concentration. One of my friends who is proficient in Haskell told me to just start coding without trying to understand the advanced concepts, but I wasn't inclined to do that as Monads were hanging over my head, and I really wanted to understand what they are before writing some piece of code.
Funnily enough, things were changed after I learned Rust. (I know I'm saying like a Rust zealot, but it's true!) After using Rust's `Option` type, and realizing its `.map()` and `.and_then()` are similar to `fmap` and `>>=` in Haskell, I finally began to grasp what functors and monads are. And that led me to understand the "implementation details" of Haskell. Before that, I thought `seq` was purely written in Haskell. But it wasn't. It is basically a compiler magic, and GHC specializes `seq` to allow eager evaluation. The same thing goes with the IO type; the monad itself has nothing to do with making I/O be done. I/O is possible solely thanks to the implementation details of the IO type, which resides in GHC. Plus, I can also guess that that part of GHC is probably not written in Haskell, because it is not possible to make side effects in "real" Haskell.
So, Rust was my admirable Haskell teacher. (Thanks, Mozilla!) It's like a stepping stone between the imperative world and the functional world. The funny thing again is, after having fun with Rust for months, now I have less reasons to go back to learn Haskell... Rust is already too powerful, performant, and easy to understand, so Haskell feels less appealing to me. Haskell had been enviable to me in the past, but now it isn't anymore, (un)fortunately.
There are lots of things in Haskell that I miss when I use rust, so I'd still encourage you to learn it. For example, concurrency in rust is OK, certainly nicer than other imperative languages, but Haskell has easily an order of magnitude more awesome to offer.
I recently wrote a library for testing concurrent code. It's basically fuzz testing of scheduler behaviour to root out race conditions. In most languages, you'd need a special compiler or runtime to do this, in Haskell it's a library.
I can definitely say that my short sidequest to Haskell began and ended with trying to figure out what a monad was, and concluding that this was a silly first topic as well.
I am glad I wasn't the only one.
The thing that makes an 'IO t' data structure different from some arbitrary java bytecode, is that bytecode is an opaque black box, and represents commands that will be executed immediately, whereas an 'IO t' just "describes" the intended action, remains composable and is a first-class value (i.e. data structure) in your program.
It's necessary to draw a distinction between the "runtime" and "evaluation" in Haskell; it's not necessary afaik to draw this distinction in other languages, which I think is where the confusion comes from.
In Haskell evaluation is pure, the runtime is not, but evaluation is what allows you to reason about your program.
IO t is a black box too. If it's a data structure, it's a crappy one: you can't pick it apart like a list, inspect it, or do anything except sequence it.
I didn't follow what you meant about "runtime vs evaluation." To me they seem hopelessly intertwined: if you evaluate head [], you get a runtime error.
You can build a completely pure version of the IO type assuming Haskell's evaluation semantics, so long as you have one base "escape" function ffi : String -> String (or whatever serialization you want) that serves as a foreign function interface. Mind yo, the ffi function is evaluated the same way (with all the non-strictness involved) as other functions in Haskell.
There's no such thing as inpure Haskell, even in your IO monad (this is more theory than fact with GHC, for performance reasons). Basically your IO type is some instructions for the runtime. The runtime does stuff and then injects values back into Haskell, but for the language itself, values are not changing.
I have a hard time explaining it. But I think GP's point was that you evaluate main to get something of type IO (), and the runtime takes something of type IO () to do stuff with it (much like the difference between javac and java). There's some juggling going along with it...
It's not a black box in the sense you can move it around, evaluate later, not evaluate at all, etc. There is fundamentally nothing you can inspect about a general IO operation: it's a handle to a computation dealing with the external world.
I'm pretty sure you can move black boxes around in the real world too :)
There are definitely sensible things to do with "general IO operations." For example, consider `withArgs`, which replaces argv for the duration of an action. A natural approach is to pick through the received action, and replace all calls to `getArgs` with `return tempArgs`. But `withArgs` doesn't work this way because Haskell can't pick apart IO actions. Instead it uses a nasty FFI to modify a global. (It doesn't even look thread-safe: I'll bet withArgs "leaks" into other threads.)
It's also overbroad: IO encapsulates "real world" computations like deleting files, but also basic operations like getting argv! I'd argue it's a failure of Haskell that the type system does not distinguish between a function that erases your hard drive and a function that gets your command line arguments.
You either enforce 100% purity, or your language is not pure. Haskell chose to walk the former path. In that case reading global state needs to happen in the IO monad even if it is reading a couple of command line arguments. Though I completely agree it is nasty to change them in runtime, it is also assumed you know what you're doing when you tinker with them (cross-thread implications included.)
To a Haskell program, command line arguments are exactly the same outer world as your super important files.
well, maybe gray then; it's not completely opaque. You know it's type, you can apply functions to the value inside. You can pass it around like a variable and if desired, completely throw it away. the point was to show that this is different than some bytecode.
I'm not sure about the head [] example, it might actually be represented by a pure value such as _|_ (bottom) which bubbles up and stops your program when running.
Ok, so we can't use the word monad because that's bad apparently. How do we answer "How does Haskell, a FP language where composition is a very important concept, compose IO actions?". Because that's almost always done using monadic actions.
Aside, I agree on the List monad thing. I never ever use the list monad, why would I. But if I'm managing state (i.e. IO, Reader/Writer, Conduit etc), then why wouldn't I use the name of the language construct that Haskell provides to manage state?
> Aside, I agree on the List monad thing. I never ever use the list monad, why would I.
I use monad library functions on lists happily, and use the do-notation, particularly when I'm thinking in terms of non-deterministic computation and search.
I wish the author had explained what the difference between using the IO type and using the IO type via the monadic interface looked like. As a Haskell beginner, this article just makes me more confused. Can someone share an example or clarify?
If you are starting Haskell, there is a wonderful little function you can use called `interact`, which has a type `(String -> String) -> IO ()`. This takes a String to String function, and turns it into an IO type that reads from STDIN, runs the function, and writes to STDOUT, lazily. Thus, you can just write `main = interact myFunction` and get everything you need to build complex Unix pipeline utilities without needing to worry about any impurity or composition. I suggest you stick to this until you are comfortable with the basic syntax, types, lazyness, and typeclasses of Haskell.
The problem with learning about the monad typeclass first is that it is a very very high level abstraction, and abstractions are near impossible to understand without a working understanding of examples that they might abstract.
This sounds amazing. I was one of those beginners who thought I had to learn monads right away to get anything done, became frustrated, and never progressed.
I think his point is that the IO type may be an example of a monad, but shut up about the monad part because it's not relevant to learning how to use it.
You don't use another interface when thinking "IO type" instead of "IO monad". It's all about what words you use to explain the types of the interface.
For example, when you write 1 + 2 you are probably thinking of (+) being addition on integers. But a mathematician could also think of (+) as the operator from the group (Z, +, 0). Groups (a concept in abstract algebra) generalize integers, but you do not need to know group theory to do arithmetic. But it's the same operator nevertheless! Just two ways of talking about it.
Similarly, when you write putStrln "Hello " >> putStrLn "world!", the "IO type" way of thinking says: (>>) is an operator that glues two IO actions together. The resulting combined action performs the argument actions sequentially from left to right. The operator has the type IO a -> IO b -> IO b.
But you can think of (>>) in more general terms. It is not meaningful only for the IO type. It is meaningful for any type that happens to be a monad! That insight may be useful eventually, but probably not for a beginner Haskell programmer just wanting to do some IO!
The IO type can be constructed in all kinds of ways. For instance, using `putStrLn` is probably the most common
putStrLn "hello world" :: IO ()
As IO is an instance of Functor we can use that interface, for instance
fmap (const 1) (putStrLn "hello world") :: IO Int
That would be using IO's functorial interface. As an extension to a functorial interface we also have a monadic interface. This affords us a "most boring" way of creating new IO values
-- very different from putStrLn
-- it doesn't actually represent *doing* anything at all
return "hello world" :: IO String
and also, finally, a way at getting at values "inside" of IO and therefore "compose" values of IO
So, the emphasis is that IO is "just" another type. One shouldn't refer to "the IO monad" as anything privileged unless you're actively referring to using (return, >>=) with IO. Generally "the IO monad" explains very little of IO's power as "being a monad" is a very small part of any type's interface.
---
To be clear, here are some other interfaces we can use from IO (besides just other IO value constructors)
* combining IO with exception handlers with
catch :: IO a -> (e -> IO a) -> IO a
* forking new threads with
forkIO :: IO () -> IO ThreadId
* embedding IO into another type with
-- extremely common
liftIO :: MonadIO m => IO a -> m a
* converting an IO action into an asynchronous action with
async :: IO a -> IO (Async a)
* assigning a IO action pointed at by a finalizer to a memory pointer with
newForeignPtr :: FunPtr (Ptr a -> IO ()) -> Ptr a -> IO (ForeignPtr a)
Think of a value with type (IO String) as "a computation that interacts with the outside world and then returns a value of type String", and similarly (IO ()) is "a computation that interacts with the outside world and then does not return a value. putStrLn has type IO (). getLine has type IO String.
Now, imagine that you want to build up a whole program that interacts with the outside world. One easy way to do this is to sequence together a number of these primitive operations. The monadic bind operator is Haskell's way of sequencing together computations that support the monad interface (which IO does). A simple console echo program could be written as "main = getLine >>= putStrLn", using the >>= operator to feed the result of getLine into putStrLn. The two computations, sequenced together, form the definition of the whole program.
The monadic bind operator gets awkward if you're trying to combine lots of these statements or if you need to assign their results to variables, and so do-notation was added to the language:
main = do
youTyped <- readLine
putStrLn youTyped
This is because a monad functions just like an overridable semicolon: it's a way for you to define how two computations, specified in sequence, will execute. For the IO type, this is specified by the language runtime to be "execute the first, then execute the second", but it doesn't have to be this. For the Maybe type, for example, it's "Execute the first, but only execute the second if the result of the first is not Nothing", and for the Error type, it's "Execute the first, but propagate the error if it returns one."
A key point the article is making but fails to mention is that these values of the IO type are just ordinary values. The "normal" thing to do is execute them in sequence, but you don't have to do that. You can, for example, map putStrLn over a list and then sequence that with mapM:
main = mapM putStrLn [1,2,3,4]
Or you could just map putStrLn over a list, and then pass the resulting list of IO values around to some other filter for further processing before sequencing. A somewhat roundabout way of defining FizzBuzz, for example, would be:
subst (i, a)
| i `mod` 15 == 0 = putStrLn "FizzBuzz"
| i `mod` 5 == 0 = putStrLn "Buzz"
| i `mod` 3 == 0 = putStrLn "Fizz"
| otherwise = a
makePrint i = (i, putStrLn $ show i)
main = sequence_ $ map (subst . makePrint) [1..100]
See what I did there? The numbers 1 through 100 are each first passed through makePrint, which pairs them with an IO action to print their string equivalent. The result of that is passed to subst, which filters out multiples of 3, 5, and 15 to instead become actions that print "Fizz", "Buzz", and "FizzBuzz". The original putStrLn calls are dropped on the floor, or more accurately, never executed because Haskell is lazy. Finally, all of the resulting actions are sequenced together with sequence_, which is a standard library function to execute a bunch of monads and throw away the result. The result of putStrLn is just a value, and can be manipulated like any other value.
This is how many of the cooler monadic libraries (eg. stateful CGI programming with WASH, parser combinators in Parsec, or bytecode JIT generation with LLVM) are implemented.
This is a fair reflection on one of the biggest barriers to newbs learning Haskell (which I've tried to do a couple of times and failed, in part because the syntax is weird and poorly explained, in part because the tutorials suck, and mostly because I'm not very smart, or so I'm told.)
The Haskell newb gets told two things very early on:
1) Haskell is pure pure pure!
2) Haskell can do IO via monads!
These statements are contradictory. Haskell is either pure, or can do IO (via monads or anything else.) No pure language can do IO. Period. End of story. Any claim to the contrary is simply, utterly, completely false. I don't care how much you wish it was true, or think it's possible to finesse the issue. You can't. And the worst thing you can possibly do is introduce a newb to the new language via a contradiction.
It's like idiots who introduce "negative resistance" with the up-front claim that a device has "negative resistance" without first introducing the generalized concept of resistance that the notion of "negative generalized resistance" depends on. Ordinary, unqualified "resistance" means V/I, not dV/dI, and if you use the unqualified term "resistance" in any other way you are deliberately and with malice aforethought creating confusion. Same goes for "negative temperature", doubled.
Want to teach Haskell? Start like this: "Haskell is an effectively pure language that hides side-effects behind opaque interfaces that return new objects that represent the modified state of the world, rather than old objects that have been modified. This trickery lets us reason about Haskell code in most cases as if it was pure even though it is not."
Claiming purity where there is none is terrible pedagogy. The newb will be confused and have a much harder time getting their head around the language. They will realize that the insiders are working in a different conceptual universe than they are, but won't see any way into that universe and will rightly assume that the insiders are being deliberately obtuse for the sole purpose of excluding newbs.
I am, as stated above, extremely stupid. As such, I am pretty ordinary in this regard. If you want people to learn the damned language, you have to cater to stupid people like me. So discard, play-down, reject and ignore the myth of purity, because that's what it is. A myth. Haskell is not pure. In a pure language the return value of a function depends only on its arguments, but any function that does IO can return any value whatsoever, regardless of what its arguments might be.
I'm sure Haskell purists will respond to this insisting that no, Haskell is really, truly, purely pure... for a certain definition of "purity". It isn't. Monads are a hack to cover necessary impurities. They are a good hack, but to redefine "pure" in such a way that accommodates monads--and this is what you have to do--is just like redefining "resistance" to accommodate "negative resistance". It's perfectly reasonable to generalize the definition of common terms, and perfectly confusing to use the generalization to imply that you mean "just perfectly ordinary resistance" when talking about "negative resistance," or "just perfectly ordinary purity" when talking about "purity with side effects hidden behind monads."
There is no particular value, use or virtue in claiming Haskell is "a truly pure functional language". Claiming the language is "effectively pure for most practical purposes, so you get the provability of pure functional programming by hiding side effects behind monads" seems far more useful. Why the Haskell community is so wedded to the myth of purity--and therefore so insistent about putting monads front and center, as the article rightly decries--is not clear. But the language and the community would be better for putting the myth and the monads behind them.
To go ahead and be that Haskell purist... in normal Haskell code there are no functions which execute side effects in the process of computing their results. Even a function like
putStrLn :: String -> IO ()
merely produces an IO value. It is only when such a value is sequenced into something presented to the RTS as `main` that the effects are realized.
This seems like pedantry, but it allows you to construct and compose IO values in many ways (besides just sequentially evaluating them). This ends up giving you "macro like" powers.
Thanks for this answer. I think it points clearly to the source of confusion.
To be pedantic back at you, I'm pretty sure your explanation translates to: "So long as you never run a program, Haskell is a pure language."
Pedantically speaking, you can't have it both ways. Either the "Haskell" of the claim "Haskell is pure" includes the RTS or it does not. If it does, it is not pure but can run programs. If it does not, it is pure but cannot run programs.
A programming language that you can't run programs in is not very useful or interesting, so I'd really think it was a better move to define "Haskell" in such a way that programs can be run, and accept that under that definition Haskell is impure, although it retains a kind of effectively pure formalism that allows provability and such, which is awesome.
Either way, it is really important to emphasize in any description of the language that "At runtime Haskell is not pure". Otherwise newbs like me will simply be confused by the contradiction between the claim "Haskell [considered as an unexecutable language] is pure" and "Haskell [considered as a set of instructions to the RTS] is capable of doing IO". The two completely different meanings of "Haskell" must be made clear if profound confusion is to be avoided.
Eh, I don't care much about names. Purity-up-until-RTS being taken seriously makes it easy to think of computations both pure and impure. I get to choose whether to interpret IO as an imperative, impure language subset of Haskell or as a pure type. Both have their advantages.
If your language is impure then you usually can only choose the former.
Here's the outline of what the middle of a tutorial could look like, following this advice:
Here's how to print a string (putStr, putStrLn). Here's how to print something other than a string (show, print). Here's how to print two somethings in a row (>>). Here's how to read something and print that (>>=). Here's how to print something in a function other than main (:: IO ()). Here's how to read something in a function other than main (:: IO a, >>= again, return). Oh by the way, it turns out that pattern (return, >>=) is so common that Haskell gives it a special syntax (do notation). For instance, it works with the previously introduced Maybe. And with List. Remember typeclasses? You can write functions that work with IO, Maybe, and List (Monad m => ...).
Even when actually talking about monads, the explanation should come first, and the term should come later. "It turns out these operations are a common enough pattern that Haskell gives them special syntax..." comes across much more sanely than "now we're going to introduce something [complicated] called a monad".