What Is IO Monad? (2018) [video]
youtube.com
youtube.com
The Haskell ecosystem has improved quite a lot in recent years: E.g. there's now Haskell Language Server which provides very nice autocompletion in Editors like VSCode. Very recently support for dot-notation has been added, so you can now write `someValue.someField` as in other languages. In the web dev space we now have IHP, which is Haskell's take of Laravel/Rails/Django https://ihp.digitallyinduced.com/ (Disclaimer: I'm founder of the company that makes IHP)
So if you have checked out Haskell in the past already, you might want to give it another try! :)
It is really a pain that any beginner in the Haskell language is learning what seems the obvious datatype for strings, i.e. String, and later has to unlearn that again.
https://discourse.haskell.org/t/hf-tech-proposal-1-utf-8-enc...
Fortunately this proposal has been accepted, but I don't know the timeline for its implementation in GHC.
Until then, working with UTF-8 is kind of convoluted
https://serokell.io/blog/haskell-with-utf8
Also: libraries need to be fixed to accept Data.Text instead of String. IsString helps (it's a typeclass that contains all string types) but only if APIs take it instead of defaulting to String. Adding random string conversions to cope with legacy APIs is very annoying.
This work was completed a while ago.
https://hackage.haskell.org/package/text-2.0/changelog
> I don't know the timeline for its implementation in GHC.
Data.Text is a library, not a compiler feature, so completing it was not tied to GHC.
If we have a long-running process, we can block the thread and wait for it to return us a result, but that's far from ideal.
Instead we return a "promise", a structure that we can provide with a callback (using the `then` method on the promise) to manipulate the result of our asynchronous processing.
The types look something like this:
`doSomeAsync(T1): Promise<T2>`
`Promise<T2>.then(T2 -> T3): Promise<T3>`
Let's look at the type of monad `bind`, `>>=` in Haskell:
`M a -> (a -> M b) -> M b`
The types of `bind` and `then` are very similar! `Promise` is (approximately) a monad.
The fact that we can't just "get" the result of the Promise and instead need to provide a callback is a core part of my understanding of the use of monads in Haskell -- they are used in Haskell to represent the result of a non-deterministic effect.
You can't (or shouldn't) just get the value out of a monad, in the same way that you can't just get "the" value out of an array -- it doesn't make much sense to talk about.
The use of monads to represent non-deterministic effects is a software engineering choice. The "monad" itself is just the structure of computation where you have a wrapped thing `M a`, to which you apply a "callback" `a -> M b`, to receive a monadic result `M b`. There are also some laws which a well-behaved monad needs to follow.
The point of this is that even though that `b` might never eventuate, or might come about later, or might be multiple values, the `M b` can be treated as a plain-old function result and the type checker is able to make stronger guarantees. Pretty handy when your language is based around pure functions.
Side note: async/await is pretty much do-notation for Promises. You write imperative-looking code and it gets magically turned into asynchronous callbacks for you.
- maps type x to type Promise<x>
- maps function x -> y to function Promise<x> -> Promise<y>
Both of these exist:
`Promise<T1>.then(T1 => Promise<T2>): Promise<T2> // equivalent to bind`
`Promise<T1>.then(T1 => T2): Promise<T2> // equivalent to map`
Here is a series he did on developing a Forth-like language from scratch, up until it's self compiling (and a few more videos after that):
https://www.youtube.com/watch?v=8QP2fDBIxjM&list=PLpM-Dvs8t0...
Here is a series on doing advent of code in HolyC in templeOS:
https://www.youtube.com/watch?v=MMqd-6wNJQ4&list=PLpM-Dvs8t0...
Just look at the breadth of videos, from openGL to Excel:
https://www.youtube.com/c/TsodingDaily/playlists
I do wish he would do revisit more haskell :)
I can't find the page, unfortunately.
In essence, think of a monad as something that takes a callback function. The callback function returns something. The monad calls the callback, and then does something based on the result. Once you get that into your head, it becomes a lot easier to understand what's going on.
(Of course, the theoretical underpinnings are still very interesting in and of themselves! They’re also a source for new patterns — things like free monads and comonads are very, very useful indeed. But it’s entirely possible to program Haskell without knowing a thing about category theory; most Haskellers don’t.)
I think that it’s an interesting side hustle and if people enjoy playing with it more power to them, but most of Haskell is driven by language enthusiasts and researchers who have no eye for practicality. Everything is done for its own sake, such as whether something can be done through derivation via HKTs etc. A lot of it comes down to it being an excuse to play with math rather than write programs. The way that it’s oversold as the solution to all bugs is based on an understanding of society that doesn’t match with reality, nor the needs of the modern programmer.
So what is an IO Monad? It’s a clever trap to get you to spend your time getting a poor man’s degree in category theory when you could be studying how computers really work.
But I suppose it depends on the environment you end up with. All the Java I did was with people where you could get away with murder in the code. All the Haskell I did was with people who carefully consider network failure, memory usage, readability, timeouts, pathological logging loops, etc., and would push back on code that isn't easy to use, maintain, and troubleshoot.
And funnily enough, the time I've spent really learning shell/awk/sed (pretty much the exact opposite of Haskell!) was one of the best investments of my career!
I also almost mentioned that I think language extensions are one of the things that hold it back the most from being more than a research project. But deciding what set of extensions are needed to make the language practical is probably a difficult problem for anyone to solve.
If you mean for the compiler writer, sure, C is very practical for someone who wants to make a compiler. But I'm afraid people might read your comment as saying C is practical for writing applications.
I understand why Haskell will never become the language of industry. Lazy evaluation, a hundred and one extensions, the fact you would need to retrain everybody... but I'm not really interested in that. Haskell is a place for ideas, not really practicality. This has been gone over time and again.
The reason I use Haskell for personal projects is because they all end up being very few lines of code compared to imperative versions, and it usually just works. For me that's enough benefit to learn it.
It would be nice if an ML language actually became popular at some point. But that won't be for a long time.
This is the entire problem with Haskell. This isn't the reality we live in. In our world there's a concrete need to understand performance at the level of the hardware and the kernel. Let's not even talk about compilers, which have to make many tradeoffs and often produce sub-optimal code without human guidance. The kernel itself doesn't always make the right decisions on where to place workloads, how to balance IRQs, moving tasks between cores, etc.
If you want to become a better software engineer, learn how your computer actually runs code, don't learn Haskell.
Edit:
It's not letting me respond to your reply directly, so here is my reply:
We all have different experiences in the industry, but my experience has been that I often need to diagnose and solve performance issues that involve using perf, ftrace, bpf, pprof, etc. I'm not saying we should all write C, I'm saying that you need to know how to look under the hood when you're using a language that does more work for you. Understanding how to look under the hood (again this is my experience) and know what you're looking at is far more important than mastering high level language concepts. You can change out the language, but the kernel, hardware, etc. remain the same. It's also important to understand these things because security is only becoming more important and attackers exploit the gaps in your knowledge. Spectre was a huge deal and it requires an understanding of how a pipeline works, how your program interacts with the cache, etc. If I ask you how to find out how may instructions your program ran, how many branches it took, how many branch mispredicts there were, if it was exhibiting false sharing, etc. could you tell me? You can't optimize something or know if you're being attacked if you don't understand how things work or what the baselines are.
Going by what you've said, it sounds like you want the industry to return to C/C++ for everything.
As for Haskell, I'm not experienced enough to make a real defense of it, but I've been told the compiler is pretty damn good. It's a decently fast language. I don't know how it compares to something like Go, and of course for very high-performance needs you probably shouldn't use it, but letting it take over isn't the worst thing in the world, and you can still use knowledge of hardware to make it faster.
But if that's not enough, just use Rust or something. It's basically Haskell-lite anyway.
You're thinking in terms of languages and not systems. If you've ever built systems at scale, you'll know that abstractions break down. The best of us write leaky abstractions. When the time comes to debug a leaky abstraction, you need to open up the hood. This means checking how many connections are open, checking whether you're flushing too often to disk, checking if your DB queries are locking up tables, checking when you're thrashing CPU, checking if you're not pooling a connection properly, etc. That's when you need to break down the abstraction layers and break out tools like iperf or Wireshark.
Languages like Haskell create such a different execution model (with monads and lazy execution) that it makes it really hard to break through its abstractions and understand what's going on.
I've had similar experiences to the author. I've written Haskell for fun and worked in Scala. Most of the folks who have your viewpoint have (in my experience) not worked in a situation where these issues come up. Either the scale of the problem is low so powerful abstractions are more powerful than deconstructing them or they work in problem domains where standards around correctness are a lot lower and programmer happiness is paramount. But if you've ever been in a position where you need to write a high-scale or high-reliability application, languages with highly abstract execution models prove more a liability than an asset. In that regard both Erlang and Rust offer a much more concrete execution model that's a lot easier to reason about than Haskell or monadic Scala offers, at least in my experience.
I’m not sure what you mean. Industrial Haskell users can and do check all of these things like their Python and C++ counterparts.
In a world where CPU is infinitely fast and has infinitely large RAM
Haskell is a top down language. It is an abstraction over a variation of typed lambda calculus. The compiler compiles haskell into this lambda calculus and then has to figure out how to efficiently evaluate the terms. One can argue that with a "sufficiently smart compiler" it can make things run fast. Unfortunately, this is not really the case and it is not easy to reason what code is being generated for what you write. Are optimizations like deforestation kicking in? There are also plenty of abstractions that the Haskell community gets excited over even though they come with performance impacts. These abstractions may be entertaining, but they are not necessarily practical. If you go deeper in the rabbit hole of FP you might find people talking about how with homotopy type theory a compiler could recognize code and substitute it with a more "optimal" version. Unfortunately, a mythical "sufficiently smart compiler" does not exist and even if it did the compile time would be impacted in trying to optimize your programs.
The "how computers really work" argument is that languages should instead be abstractions over the CPU (or other hardware devices) instead of abstraction over computation itself (alternatively you could also see it as an abstraction over graph reduction based hardware but since we aren't running it on such hardware that is problematic). In the real world we have real concerns about resource usages such as processing time and memory. Abstractions over the CPU makes it easier to have a general idea on what the code that is being generated looks like.
It may be the case that your use case is not that sensitive to performance or memory usage so continue to have fun using Haskell. As you said Haskell is "not really practical." To your point of Haskell having very few lines of code compared to imperative version I feel that is likely just a matter of what is in the standard library / what libraries you used.
Sure, you can express nigh everything in C++, but defaults matter — a parallel algorithm may well be in the “worth it to implement” category for Haskell, and be in the “no way that we can make it work” category for some lower level language.
Don’t get me wrong, sure, the memory model of haskell may also not be the best which has probably the biggest performance impact, but I really don’t think that there is as much of a difference between comparable high level languages to haskell, so it can absolutely be a good choice for certain applications.
It is still largely serial. If you are concerned about the dependencies of the actual instructions themselves (since the CPU can use this information to execute multiple instructions at the same time) in order to optimize something you will be very concerned about being able to predict / look at / influence the code generation.
>a parallel algorithm may well be in the “worth it to implement” category for Haskell
>“no way that we can make it work” category for some lower level language.
I disagree. It is mainly a matter of having libraries available that makes it easy. If a library was made for Haskell that makes parallel algorithms easy to implement a similar library could be made for C++ or whatever to do the same thing.
Even in rust it is not as comfortable as in a higher level language - it is not accidental that where maximal performance isn’t needed GC-d languages are used predominantly in the industry.
>>“no way that we can make it work” category for some lower level language.
> I disagree. It is mainly a matter of having libraries available that makes it easy. If a library was made for Haskell that makes parallel algorithms easy to implement a similar library could be made for C++ or whatever to do the same thing.
This does not match my experience. Haskell encourages you to write code in a way that makes parallelism much easier and less likely to bite you (pure functions and immutability everywhere). I've added parallelism to Haskell code that was written without really thinking about parallelism by changing 2 important lines (and trivial changes to a few places in the same file). I can't imagine it being that straightforward if the code had been in C++.
How do you explain the use of Haskell or Haskell-like languages in High Frequency Trading?
Haskell is being used in the industry with great success, yet this myth continues that it can only be used in academia. Puzzling.
I wonder what is your view on functions and local variables? Do you find structuring programs with them useful or just an academic exercise?
I had the "privilege" to work with assembler programs, written in 1970s and 1980s, that were mostly one big procedure, with a huge bunch of shared global variables. Back then, it was very efficient way to program (that's why it was used), and down to metal. But it was unmaintainable mess.
(And that's what we want from a compiler - to transform program from maintainable and somewhat abstract description down to unmaintainable spaghetti that can be, however, executed very efficiently.)
Pure functional programs are just taking it step further, saying that most functions shouldn't need full access to their execution environment (operating system). The result is, supposedly, even more maintainable programs.
The issue with your POV is, IMO, that you lack imagination for how vast the industry really is. There are millions of problems that are being solved with Python, PHP, Java, you name it, that can as well be done with Haskell. And constraining those solutions through the lens of "how computers really work" is unpractical, theoretical BS.
But let’s take a compiler backend or a JIT compiler — you may very well need much more powerful abstractions for these, and having the language help you with that can be a huge boost. In this area, the concepts you may have learned would definitely apply much more often.
I have never seen an `is` operator in a shell so my understanding stopped right there, which is a pity as I felt I was getting somewhere.
It's so because monads follow the exact same pattern by definition, but the different types of monads just inject some additional behaviour and constraints as well.
Different patterns from this are no longer monadic.
The "magic" part of this is why different types of monads don't compose well.
Of course, this is overly simplified but might help for understanding.
https://askubuntu.com/questions/172982/what-is-the-differenc...
[0] https://www.youtube.com/playlist?list=PLe7Ei6viL6jGp1Rfu0dil...
# Lambdas, Functions, Functors and Monads
Even most OOP programmers are now familiar with map and, having used it quite a bit, have an intuitive understanding of what any given call to map will do.
Does anyone know a bit more about map, what it is and where it came from?
## Category Theory
A lot of Functional Programming derives from a field of Mathematics called Category Theory.
Very briefly, Category Theory is the study of functions, where "function" refers to a process that associates to each element of a set X a single element of a set Y.
Pure functions have only inputs and outputs- no access to anything other than what was sent in, and no ability to mutate what was sent in or generate other side effects.
Now, I suck at math, but fortunately we don't need to know math to learn more about concepts programming languages have borrowed from Category Theory.
Learning about it can make us better programmers if we go from an intuitive, but maybe a bit vague, understanding of things like map to fully understanding them!
## Functor (map)
A *Functor* is anything that can be mapped over. Map, the defining function of a Functor, is quite simple.
interface Functor<A> {
fun <B> map (function: (A) -> B) : Functor<B>
}
This just says> Given a Functor of A, when map is called with a function of A to B, return a Functor of B
### An example
This is all just a bunch of mumbo jumbo, but in our day to day programming, Functors (or potential Functors) are everywhere!
Lists are a great candidate for a concrete example.
class BoringList<A>(val backingList : List<A>) { }
The above class is just a wrapper (with no added behavior) around a list. Let's make it do something interesting by declaring it a Functor. class InterestingList<A>(val backingList : List<A>) : Functor<A> {
override fun <B> map(function: (A) -> B): InterestingList<B> {
//implement - DON'T CHEAT by simply calling map on backingList
}
}
fun main(args: Array<String>) {
//experiment
}
Example implementation (very imperative, but that's ok) class InterestingList<A>(val backingList : List<A>) : Functor<A> {
override fun <B> map(function: (A) -> B): InterestingList<B> {
//the current InterestingList is of type A but we need to return
//a InterestingList of type B, so first, make a new backingList
//of type B
val transformedList = ArrayList<B>()
for (item in backingList) {
//apply the provided function to each item
val transformedItem = function(item)
//add them to the transformedList of type B
transformedList.add(transformedItem)
}
//return an InterestingList of type B
return InterestingList(transformedList)
}
}
fun main(args: Array<String>) {
val interestingList = InterestingList(listOf("foo", "bar"))
//when calling map on InterestingList<String> with a function: (String) -> Int, we get InterestingList<Int>
val mappedInterestingList = interestingList.map { string -> string.length }
}
### Other examplesNow, we've used List as an example, but remember: Functors (or potential Functors) are everywhere! It's easy to think of Functors as "containers", but it's more useful to think of them as a "computational context", since eg Options and Futures can also have map.
* The Functor of List applies the function to all elements in the list
* The Functor of Option applies the function if it's non-empty. (or rather, any function applied to Nothing is still Nothing)
* The Functor of Future applies the function to a result once a result is available.
### Functor laws
Now, can someone think of a few important things to keep in mind when implementing map?
Eg, what if your implementation of map for a List always returned an empty list, or a list twice the size of the given list? Or what if it mutated the items it mapped over, or called a bunch of other god knows what functions? That would be not so good!
So when implementing Functors you actually have to observe the Functor Laws. There is nothing mysterious about these laws; their role is to guarantee map behaves sanely and actually performs a mapping operation (as opposed to some other nonsense). The first law is:
map id = id
id is the identity function, which returns its argument unaltered. The first law states that mapping id over a functorial value must return the functorial value unchanged. Next, the second law: map (g . f) = map g . map f
It states that it should not matter whether we map a composed function or first map one function and then the other (assuming the application order remains the same in both cases).## Monad (flatMap)
A *Monad* is anything that can be flatMapped over. Sounds scary! It's actually surprisingly easy to understand, but can be a little more complex to implement. Don't feel bad if you don't get it right!
FlatMap, the defining function of a Monad, is quite simple.
interface Monad<A> {
fun <B> flatMap (function: (A) -> Monad<B>) : Monad<B>
}
This just says> Given a Monad of A, when flatMap is called with a function of A to Monad B, return a Monad of B
If you recall Functor
interface Functor<A> {
fun <B> map (function: (A) -> B) : Functor<B>
}
Which just says> Given a Functor of A, when map is called with a function of A to B, return a Functor of B
The two are EXACTLY the same except instead of a function of A to B, it's a function of A to Monad B.
### An example
Let's return to the concrete example of the list: if you call map on your list with a function of A to List B, what will you end up with?
You would end up with a list of lists! And all flatMap does is "flatten" that into a single list.
interface Monad<A> {
fun <B> flatMap (function: (A) -> Monad<B>) : Monad<B> //yes!
fun <B> flatMap (function: (A) -> Monad<B>) : Monad<Monad<B>> //no!
}
Implementing is left to the overachievers.### Other examples
Remember: just like Functors, Monads (or potential Monads) are everywhere!
* The Monad of List flattens lists of lists.
* The Monad of Option allows chaining many potentially absent things together, short circuits if any of them are empty, and returns a flattened end result.
* The Monad of Future allows chaining together computations that don't have a result yet, and returns a flattened end result.
### The Monad laws
Just like when implementing map for Functors the implementation must satisfy the Functor Laws, when implementing flatMap for Monads, the implementation must satisfy the Monad Laws. I will not get into them here, but for anyone who's interested this is a great resource https://en.wikibooks.org/wiki/Haskell/Understanding_monads
## A missed opportunity
As we've seen, map and flatMap really shouldn't be these random functions that sometimes get added to things and sometimes not. Functional programming languages usually have an interface Functor, and an interface Monad etc which all classes that implement map and flatMap extend, and there's no reason why it shouldn't be like that in Java or Kotlin as well.
Unfortunately, neither Java nor Kotlin do this, which is kind of a missed opportunity.
## But why?
Most of us instinctively reached for looping language constructs like `for` and `while`.
So since we're so used to that, where's the benefit in all this new map, flatMap etc nonsense? It's difficult to understand, hard to read etc etc.
A few differences could be pointed out.
### An example
//this looks fine but will actually blow up, can you tell why?
int[] array = {1, 2, 3};
int[] doubled = int[array.size];
for (int i = 0; i <= array.size; i++){
doubled[i] = array[i] \* 2;
}
//even if it didn't blow up, there's still a lot of noise about *how* to
//double that is completely besides the point of *what* we actually want,
//which is simply to multiply the items by 2
#### Imperative vs Declarativeloops describe in detail how to do something while map hides implementation details and let's you focus on what to do. And since describing in detail how to do something isn't required, the risk of bugs in the how part is completely eliminated.
#### Stateful vs Stateless
loops are literally stateful operations where we're, well, looping through a collection item by item (in a certain order) using something to keep track of how far we've looped. Sometimes the state matters to what's happening inside the loop, eg when we find what we're looking for and return early. Mapping operations have no such state, so the risk of bugs due to state are completely eliminated.
#### Meaningless vs Meaningful
loops have no real meaning. Eg, it's possible to loop through things and do nothing, mutate the items as you loop through them, mutate the collection you're looping through, return something from an enclosing method... Loops can do anything. Map on the other hand is a specific thing, has a specific use case, does no more and no less, makes guarantees as to what it does and doesn't do etc etc. This allows us to reason about code more effectively than with loops where we have to play compiler and debugger and keep a bunch of state in mind all the while trying to understand what's actually being done inside.
These are just a few examples, I'm sure more could be thought of!
Neither does Scala.
Link: https://gist.github.com/androidfred/6a0c85dd3bf75eb444c5ca13...
IO monad is where the environment (context of execution) is just your entire operating system. In imperative languages, this is a normal state of affairs, but functional programmers view that with similar suspicion that most programmers of today view global variables.
That's why in general "different monads don't compose", because it's like trying to compose two different environments (as we know, for example, porting between different OSes is not trivial). You can define it (with monad transformers) but it's often a lot of work.
[1] https://hackage.haskell.org/package/TypeCompose-0.9.14/docs/...
Some monad transformers, like `MaybeT m a`, are just wrappers around a composed `m (Maybe a)`, but others, like the continuation passing transformer `ContT r m a = (a -> m r) -> m r`, are very much not.
Monads are not the only way of tracking effects in the type, but they are one way. For example, Java has "checked exceptions", which also represent an effect in the type system.
Monads also allow one to assemble a computational ready for execution later. One can therefore write an Async library, without needing baked-in language support.
This is relevant for IO because it can maybe fail.
It is “needed” due to pureness - without haskell’s unsafe, there is no other way to do side-effects[1]. But you are otherwise right. The reason it is beneficial in FP languages is that the optimizer can reason very well about these pure expressions.
[1] though execution itself can be considered a side-effect, and haskell code can throw exceptions, so it is more like compartmentalizing the majority of effects
Laziness is also better for composition and local reasoning because you can refactor out any subexpression.
https://www.microsoft.com/en-us/research/publication/wearing...
Not, mind you, merely the possibility of laziness; as you say, you can do that without monads or purity. But with pervasive laziness it becomes difficult to reason about the precise order in which thunks will be evaluated and be sure that (say) the warning will be printed before the missiles are fired instead of after.
With purity it (mostly) doesn't matter the precise evaluation order, so (again, in the setting of pervasive laziness) we want that wherever we can have it! But at some point we need to actually fire those missiles and print those messages. The Monad interface for IO, in chaining together effectful computations with bind, makes the input of later computations depend on the output of earlier computations such that the regular data dependencies between the thunks ensure that things happen in the order we asked for.
The parenthetical is extremely important too. What looks like a function call in Haskell is dispatched based on the types of its arguments (yes, functions take just one argument, but be that as it may). This is important to understand. The "methods" available depend on the types of the values being passed to them.
Hahaha! I swear this is exactly how I feel halfway through every monad tutorial I find.