Effectful Haskell: IO, Monads, Functors
slpopejoy.github.io
slpopejoy.github.io
More generally, I think one problem the Haskell world has with attracting more developers is that it's dominated by people with a very "pure maths" mindset. The people developing and advocating Haskell often enjoy exploring the abstraction possibilities and interactions for their own sake, just as a research pure mathematician studying some form of advanced algebra might. Of course, there's nothing wrong with that, and as a platform for programming language research it's probably an asset to have a lot of such people involved. However, most other people, even those of a technical persuasion, do not find such a purely theoretical approach interesting. They want motivation for any theory they are learning and they want practical applications to show why it's relevant.
As Edward Kmett says in more detail in [1] - the point of calling a monad a monad is not to confuse the reader, but to unlock 70 years of documentation on the concept for the reader. How many programming concepts/tools/libraries do you use that have 70 years of documentation?
Being abstract is profoundly different from being vague.
[1]: https://yow.eventer.com/yow-2014-1222/stop-treading-water-le... (around the 20 minute mark).
This is probably more off-putting than you suspect
By the way, while I'm not sure the monad concept would be useful for all languages, I believe monoid really should become common parlance. It's such a simple and useful idea.
This debate about naming monads is pretty tiresome after so many years, if one called it "computation builder" it wouldn't change their structure or convey any notion of the laws any better than term monad. A monad at it's core is a set of algebraic relations.
If you want a mathematical exposition. "Category Theory" by Awodey page 265 is a concise description.
Sure, their instances (implementations) are values (dictionaries of functions which are implicitly passed around) but thats beside the point when learning.
It's harder than it has to be, but it's also more powerful than it has to be. Because against the right kind of problem, it'll be just barely powerful enough.
The set of natural numbers N is not a monoid; {N, +, 0} is a monoid. The set may, at best, be "monoidal" (i.e. there exists associative binary operator <> and a set member ZERO such that for all elements of the set, ZERO <> x == x <> ZERO == x).
So as a beginner, it doesn't even help to try and read the "70 years of literature" on the subject, because what you read there does not match (I'm reading about a set, an operation and an element of the set - and all I have here is the actual set. Where are the operation and the element? Oh they are defined and passed implicitly for the type. Oh so the type isn't the monoid, its at best MONOIDAL)
It's relatively common to refer to the underlying set as a monoid (or whatever structure you're talking about) if it's clear from the context what the operations are, though.
> The set may, at best, be "monoidal" (i.e. there exists associative binary operator <> and a set member ZERO such that for all elements of the set, ZERO <> x == x <> ZERO == x).
That's a pretty useless definition, though, as every non-empty set trivially satisfies that condition (just pick any element as the zero element, and let the binary operation be the constant mapping to that element). Also, “monoidal” usually means a monoidal category, which is something very different from the underlying set of a monoid altogether.
Relatively common even outside Haskell?
It's common throughout mathematics. So common, in fact, that there's even a wikipedia article on the phenomenon[0].
“Common examples occur when speaking of compound mathematical objects. […] Similarly, one often refers to a group (G, \star) as simply G when the group operation is clear from context.”
This is very important, and something that took me a bit to grok, and definitely got in the way of understanding for a while.
One thing you can do - and I don't know whether it makes sense to teach it this way - is think of the typeclass constraints as actual arguments (absent optimizations, that is literally the case anyway - GHC passes instance dictionaries). In that case, "the monoid" is that dictionary, and the type referenced is the carrier set, and a value of that type is an element of the carrier set.
Likewise with "Mappable": in most other languages, `map` is only an operation on lists or list-like things. `ask` in `Reader` is implementable with Functor only: by what tortured metaphor does `Mappable` help you understand that? Also it sounds like Java. Yuck :)
Names are a bikeshed. Monad, Applicative and Functor have the advantage of at least being rigorous, I can't see any other name being better.
It makes me wonder who ends up using this language for anything serious in even the slightest time crunch...
As spopejoy says, Monad is a typeclass, that means every Monad is a type on itself. So you you can get by just learning every monad for its use without ever needing to know the theory that binds them.
The Maybe Monad is just an option type, the list monad is just a list and the IO monad is just a sequence of operations that gets returned to the runtime from the main function. Who cares that they all share a typeclass?
You might come back to Haskell later and it won't seem like such a big deal. (I use Haskell for serious work and love that a quick patch doesn't suddenly start breaking things in 10 other places - it's a real time-saver!)
Monads are hard, let's call them
- FlatMappable
- AndThenable
- Joinable
- Chainable (or, Daisychain)
- Computation Builder
If that were so, monad and functor tutorials would be as follows:
Functor tutorial: imagine it's called "mappable". End of tutorial.
Monad tutoral: imagine it's called "computation builder". End of tutorial
There are some major benefits to the Haskell community's approach of naming abstractions after the math (where a suitable mathematical abstraction exists).
First, as has been mentioned, there are existing treatments of the objects in question, some interesting results there can be ported over to programming, and intuitions there lead new and sometimes useful places. Some newcomers will even be familiar with the concepts already - this is very few people for monad, but far more for monoid and semigroup. It avoids erecting an unnecessary wall between programming knowledge and mathematical knowledge.
Second, it changes the character of a particular kind of discussion: there is never ambiguity about whether a newly considered operation on a type "really" is "appending" - is it associative? does it have an identity? you've got a monoid. This means it's very clear what you can and cannot assume around a particular interface. Questions about whether "functors" are well thought of as "containers" or "mappable" or whatnot are clearly only questions of pedagogy.
I've always assumed that Haskell's compiler has a built-in rules about IO having side effects, but I never actually bothered to find out.
When I first started with Haskell, one of many small epiphanies was when I realized that the IO monad itself doesn't actually do anything. bind and return don't do anything except wrap and unwrap values, and there's nothing that checks if you are in some kind of "IO context" to permit things like file I/O. There's no magical "side-effectful computation chain engine" behind the scenes.
At this point you'll either accept the state of affairs, or want to go deeper down the rabbit hole, at which point you become more an academic than a programmer per se.
In a very precise sense what groovy2shoes is saying is exactly right, and you need to understand that truly strange weirdos inhabit IO () whereas, as you point out, only one thing, modulo non-termination, inhabits ().
There is some magic around the IO type, but it's not needed to answer your question.
If you look in GHC.Types in the package ghc-prim, you can find the definition of IO:
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))
An IO action is a function from the state of the world, to the state of the world plus a value. So the compiler may know it's giving you (), but it doesn't know what the new state-of-the-world will be.1) The IO action that does nothing.
2) The IO action that prints the string "Hello World" and then returns nothing.
3) The IO action that reads a string from the console, reverses it, prints it back, and then returns nothing.
4) ...
It's a common misconception to think that IO X is a wrapper around X with some "type system magic" to prevent it from being used outside IO. It's nothing like that. For example, IO String is not a wrapper around a String, doesn't contain any String inside, and cannot be converted to String. Meditate on the fact that System.IO.getLine is not a function, but a value of type IO String. Then you will understand why IO () has tons of possible values.
Edit: I find it interesting that Haskell tutorials and books generally don't try to peel away some of Haskell's abstractions. Many texts will tell you that "IO a" represents an "action" and expect you to take this at face value, intead of explaining how this is distinguishable from a mere function, and how it's implemented internally. For example, I would say one of the big eureka moments for any Haskell learner is to realize how lazy-evaluated graphs of functions can turn into linearly-ordered programs thanks to the transparent "baton passing" of monad chaining (IO in particular). But I've yet to find a text which articulates this idea very well.
data IO a = PutChar (Char, IO a)
| GetChar (Char -> IO a)
| Return a
Programs can work by constructing a value of type IO () and defining it as main, just like in regular Haskell. Note that IO is no longer a wrapped impure function, and there are no magical tokens in sight. In fact, the above implementation doesn't even have to be opaque, it can be completely exposed to programmers. Nevertheless, it's just as easy for the runtime to interpret in a sequential way, and there won't be any problems with accidentally optimizing away or memoizing anything.So for a list monad bind = flatMap fmap = map
is that correct?
Studying FP for quite a while first monad tutorial I come to fully understand finally.
> is that correct?
Yes, although Haskell calls "flatMap" "concatMap". Of course "return" is just "\x -> [x]", AKA "(: [])".
Instead of bind (">>="), you can implement Monads using "join" instead. For lists, "join" is simply "concat".
And then you can go much further into that world with the scalaz library.
One of those things was Scala's `for` (and `flatMap`). It clicked for me once I noticed how similar it was to Haskell's `do` notation for Monads.
What's surprising to me is that it would seem Haskell has no right to be this helpful for understanding production code!
* almost the same to IO, because of recursive thenable assimilation, something that was added to promises but is totally short-sighted and unnecessary; and because promises are eager which means they don't represent the action to be executed, but the value of an already executed action
For example, an array can be a monad if we define a bind method as map + flatten
Array.prototype.flatMap = function(f) {
return _.flatten(this.map(f))
}
which works like so: allArticles = authors.flatMap(author => author.articles);
return would be Array.of = function(val) { return [val]; }
The shape (type) is the same:the promise's bind (then) method takes a function that takes a value and returns a promise, and returns another promise
the array's bind (flatMap) method takes a function that takes a value and returns an array, and returns another array
the promise's return (Promise.resolve) method takes a value and wraps it in a promise
the array's return (Array.of) method takes a value and wraps it in an array.
You can get the array method types (shapes) from the promise ones by replacing promise with array (if you decide to give the methods the same name).
The implemented functionality, however, is vastly different.
Haskell is great because it's type system is rich enough to represent these concepts and not only that but enforce their usage (to a degree). Unfortunately this is like a drug, once you get a good dose of Haskell it's easy to crave an even richer type system with dependent types.
https://www.google.fr/search?q=jvtoups+monads
I hope you know lisp just enough to adapt to the lisp-1 lisp-2 namespace.