<p>The <code>IO</code> monad, or the <em>Karmic monad</em> as what it's called in the language I'm making, is a catch-all for every side-effects.</p>
<p>Introducing this monad has many benefits, mostly for encapsulating and monitoring side-effects, and for preventing security threats.</p>
<p>But what are its disadvantages? The major disadvantage I'm seeing is debugging issues. Unless something like Haskell's <code>Debug.Trace</code> is provided, it's impossible to see what's going on in pure operations. Are there any more crucial disadvantages?</p>

<monads><side-effects>
What are the disadvantages of introducing the IO monad?
CC BY-SA 4.0
2023-07-10T08:24:33.903Z
There are really two questions here, one about making a language pure and one about using a particular mechanism of allowing side-effects in a pure language.
leftaroundabout
<p>From what I understand of this topic, the &quot;IO monad&quot; was a solution for restoring a specific time-ordered evaluation to a language that did not in the first place guarantee to respect the sequence of evaluation specified by the programmer.</p>
<p>The reason a specific time-ordered evaluation is important with so-called &quot;side effects&quot; (in my view, as misnomered as suggesting drunkenness is a side effect of alcohol, when it is in fact usually the main effect), is because these side effects make changes to shared data in a way that the correct sequencing cannot be inferred purely by analysing the dependencies between the arguments and results of function calls.</p>
<p>Sometimes the correct sequencing cannot be inferred at all from just an analysis of the whole source code of the program - but can only be known by a programmer who is familiar with how the broader system works, and how the shared data is altered from outside the program in question.</p>
<p>So the existence of shared mutable data imposes a hard requirement that the programmer must, in at least some cases, be able to impose a specific order of evaluation which the compiler could not infer for itself as being the necessary order.</p>
<p>Languages like Haskell, meanwhile, do not guarantee to execute in the order the programmer specifies. They guarantee to execute only in the order which would satisfy the constraint that a function's arguments must be evaluated before it's results can possibly be computed.</p>
<p>In imperative programming, the problem is already solved because the program executes exactly as the programmer instructs.</p>
<p>The IO monad simply undoes (in a limited way) the consequences of not guaranteeing sequential execution in the first place.</p>
<p>There's no disadvantage to the existence of the IO monad, since it allows languages like Haskell to cope with scenarios which they otherwise could not cope with.</p>

CC BY-SA 4.0
2023-07-10T16:36:38.687Z
2023-07-10T16:36:38.687Z
Steve
CC BY-SA 4.0
2023-07-10T18:51:47.407Z
It's not that Haskell does not "*respect the sequence of evaluation specified by the programmer*", but rather the whole idea is that the sequence of evaluation is not important, no matter how you evaluate an expression you will always arrive at the same result. *That* is the guarantee that Haskell specifies, and it gives the compiler a great deal of freedom to optimise.
Bergi
CC BY-SA 4.0
2023-07-10T19:14:05.803Z
@Bergi, agreed. The essential point is that the compiler assumes the right to evaluate in any order consistent with the explicit dependency of results upon arguments (or, I guess, any algebraically valid rearrangement of them). The issue with mutable shared data is that it is often shared external to the entire program (not just a particular function), and therefore the compiler often couldn't possibly possess the information necessary to determine a correct order - only the programmer does.
Steve
<p>Many people believe that monad is the thing that makes I/O possible in a pure functional language. But to my eyes it has only provided some high level constructs and syntactic sugar to make programming easier. The real thing to make I/O possible is <del>lazy evaluation</del> <em>something that is not used yet could have uncertain values, which is like lazy evaluation, and could be modeled as such, to hide the special properties in the language's own logic</em>, and the high level constructs have only hided the real thing from the user. (<strong>Edit:</strong> It's in the sense that a programmer could create something very similar to the I/O inside a program, for example to define coroutines and create I/O streams between coroutines, using a format unrelated to the built-in I/O features. That is, to play around with it, and extend it freely, instead of just using it. A language could make whatever exceptions for the real I/O, but if it is that case it doesn't make it interesting.)</p>
<p>If you want to add some more features, for example, to connect the input of something to its own output, and to allow closing and reopening a stream accessed from another coroutine (i.e. to make a copy of a coroutine with the stream changed and discard the original coroutine), it might be easier to firstly think in lazy evaluation, and only consider implementing it in monads.</p>
<p>Monads could make people notice some details hidden in most languages, but it doesn't tell you everything about them. In the two examples, while you already have monads, you may still need to insert the required interfaces somewhere by yourself, possibly using what is made available by lazy evaluation. In case you don't care about these details, you may just want I/O, instead of monad I/O.</p>
<p>If someone does everything right, I believe they could implement every feature of modern object oriented programming in a pure functional language, if we don't care about the time and space complexity. Saying we are implementing something using monads might be misleading, because that sounds like if we had monads, we had everything, while in reality, it is like saying we are implementing something using classes and objects, which doesn't really tell much about its complexity. I'd say monads are successful for modeling some common ideas, but won't credit everything to it.</p>
<p>In that case, there are no pros and cons, because the different programming paradigms would be effectively the same thing. But the problem is how much time you would want to invest in it, because the currently existed languages are obviously very far from achieving this. It's difficult to comment about the pros and cons because most people would stop somewhere before fully imitating object oriented programming, but I can't know exactly where.</p>
<hr />
<p>The general idea is, the concept of monad itself doesn't capture everything happening in the background. It is something supposed to be possible to be defined in a functional language using its own syntax, not something in the infrastructure of the language itself. I may fail to describe what is really happening in the background well, but the point is that part cannot be ignored, or the I/O might become very restrictive.</p>

CC BY-SA 4.0
2023-07-10T12:50:13.367Z
2023-07-10T16:10:00.433Z
2023-07-10T16:10:00.433Z
user23013
CC BY-SA 4.0
2023-07-10T14:30:03.827Z
The earliest versions of Haskell used nothing but lazy evaluation for doing "IO", but this was soon discovered to be very impractical.
leftaroundabout
CC BY-SA 4.0
2023-07-10T14:51:11.820Z
@leftaroundabout Any resources about the earliest versions? While it might be difficult to program in nothing but lazy evaluation, I don't think there is anything to prevent a programmer to define something monad-like by themselves.
user23013
CC BY-SA 4.0
2023-07-10T14:56:36.213Z
"*The real thing to make IO possible is lazy evaluation*" - I don't follow. Why wouldn't the IO monad work in an eagerly evaluated language? Especially since `IO` operations themselves are executed eagerly.
Bergi
CC BY-SA 4.0
2023-07-10T15:14:13.207Z
When you say *"The real thing to make IO possible is lazy evaluation"*, are you referring to I/O in general or the `IO` type specifically?
David Young
CC BY-SA 4.0
2023-07-10T15:27:54.303Z
@DavidYoung Changed all to I/O to reduce confusion, though I might be still wrong or still not have expressed what I intended clearly.
user23013
CC BY-SA 4.0
2023-07-10T15:33:45.987Z
@user23013 I think might see part of the misunderstanding here. If I'm understanding you correctly, you are saying that the language needs some way to represent `IO` actions that are not (yet) executed (hence the initial things about laziness). This is so they can be combined into new actions without being executed right away. Is this roughly what you're saying? However, most languages already have this. In fact, most languages already have what Haskell specifically uses for this: functions (as values). You can combine functions before applying them (or or maybe you never apply them), etc.
David Young
CC BY-SA 4.0
2023-07-10T15:39:56.923Z
@Bergi Edited but I'm not sure I have clearly described what I intended. Speaking of the ideas behind the concepts, instead of the technicalities in a given implementation, the concept of lazy evaluation becomes somewhat subjective. Monads could hide lazy evaluation and use other ways to implement them, but I don't think it could retain all the good characteristics, but unfortunately I'm not able to list the potential good characteristics I'm thinking of.
user23013
CC BY-SA 4.0
2023-07-10T15:51:27.837Z
@DavidYoung Yes, this could usually be done just by using functions. It might be me having some misunderstanding about pure functional programming. In that case I might have used some wrong terms, but I can't think of another term. I think someone could also just use normal functions with side effects for I/O, where wrapping something in a function is most useful. But it's all about removing side effects.
user23013
CC BY-SA 4.0
2023-07-10T18:20:39.027Z
See https://stackoverflow.com/questions/17002119/haskell-pre-monadic-i-o. The link it provides to the Haskell 1.2 report is dead, but the report can also be found [here](https://dl.acm.org/doi/pdf/10.1145/130697.130699). (The formatting of that document is... different; a better copy may be available somewhere else.)
chepner
CC BY-SA 4.0
2023-07-12T14:42:50.987Z
@chepner Thanks. That's exactly what I expected it to be. But I don't see the part to make it supposedly impractical. I'm more interested in why they decided to move away. (For some reasons, I assumed it very long to read and only looked closely when I had time.)
user23013
CC BY-SA 4.0
2023-07-12T15:25:12.137Z
Within a single function, I think it would be simple enough to correctly pair items added to the `[Request]` list that `main` returns with the elements of the `[Response]` list that `main` receives as an argument. But as you start trying to spread I/O across multiple functions, maintaining the correct correspondance gets more difficult.
chepner
<p>There are a few disadvantages, but they are really facets of one underlying design decision: the <code>IO</code> monad is a <em>framework</em>, and if there's one thing we know in the software business, it's that frameworks generally don't play nicely if you try to mix them.</p>
<p>As always, this is mostly subjective; one person's disadvantage is a price that someone else is willing to pay.</p>
<h2>IO represents only one kind of procedure</h2>
<p>Side-effects aren't the only reason why you might want &quot;procedural&quot; code. Another example is mutable data structures. So you'd like to be able to &quot;invent&quot; a procedural monad even if <code>IO</code> hasn't made it to this point in the code.</p>
<p>Haskell has <a href="https://hackage.haskell.org/package/base-4.16.3.0/docs/Control-Monad-ST.html" rel="noreferrer">the <code>ST</code> monad</a> for this purpose. But now you have <em>two</em> kinds of procedural type: <code>ST</code> and <code>IO</code>, and it's not obvious how to mix them. There are ways to layer <code>ST</code> on top of <code>IO</code> (e.g. <code>stToIO</code>), and a module, <a href="https://hackage.haskell.org/package/array-0.5.5.0/docs/Data-Array-MArray.html" rel="noreferrer">Data.Array.MArray</a>, whose only purpose is to abstract two different kinds of mutable array: the <code>ST</code> kind and the <code>IO</code> kind.</p>
<p>A third interesting kind of procedure is <em>backtrackable</em> destructive update, for use with backtracking nondeterminism. Here, you might have, say, mutable arrays, but mutations can be recorded and &quot;undone&quot; on update. I don't know if anyone has tried implementing this in a language like Haskell.</p>
<h2>IO doesn't play nice with multi-threading</h2>
<p>There are lots of ways to spawn threads in a language like Haskell, but they have one thing in common: they create a <em>separate</em> real world.</p>
<pre class="lang-haskell prettyprint-override"><code>spawn :: IO a -&gt; IO (IO a)
</code></pre>
<p>This has implications. For example, mutable data structures based on <code>IO</code> (e.g. <code>IORef</code> and <code>IOArray</code>) <em>must</em> be thread-safe by default, otherwise they are not safe. You can't pay for thread-safety in these primitives only when you want it.</p>
<h2>Mixing monads and comonads is an unsolved problem</h2>
<p>Some effectful operations (e.g. simulation) may be better expressed as <a href="https://www.haskellforall.com/2013/02/you-could-have-invented-comonads.html" rel="noreferrer">comonads</a> rather than monads. But <code>IO</code> requires monads, and we are yet to work out how to write code that uses both monads and comonads together in a useful way.</p>
<p>It may be that the problem is impossible in text, because &quot;arrows&quot; are inherently diagrammatic (see <a href="https://www.haskell.org/arrows/syntax.html" rel="noreferrer">arrow syntax</a>).</p>

CC BY-SA 4.0
2023-07-10T03:04:58.580Z
2023-07-10T03:25:44.500Z
2023-07-10T03:25:44.500Z
Pseudonym
CC BY-SA 4.0
2023-07-10T16:24:26.170Z
`STM` gives a form of backtrackable mutation. In logic-programming terms, `orElse` is a choice point, `retry` is failure, `TVar` is a logic variable, and the transaction record of an `atomically` block is the *trace* of instantiated logic variables that will be reset upon backtracking. As for comonadic & monadic programming, they *can* be mixed, using forms like `(Comonad w, Monad m) => w (a -> m b)`. However, you commit to an evaluation strategy based on where you put `w` & `m`, and sometimes you can’t use a particular `w` & `m` together, if one doesn’t distribute over the other.
Jon Purdy
CC BY-SA 4.0
2023-07-11T00:30:40.510Z
That's true, but in my head, `STM` is designed for short transactions, not for search. Specifically, in hardware transactional memory, the transaction can fail on a cache miss. And I'm certainly not saying that monads and comonads *can't* be mixed but rather, to quote the intro, they don't play nicely together.
Pseudonym
CC BY-SA 4.0
2023-07-11T00:54:02.193Z
Right! Just making a couple observations, not trying to contradict you. I doubt STM would be *good* for search, but you made me notice a similarity I hadn’t before.
Jon Purdy
CC BY-SA 4.0
2023-07-11T01:18:00.177Z
Using transactions to implement search sounds like the infamous Prolog failure-driven loop. https://stackoverflow.com/questions/13057428/prolog-failure-driven-loops
Pseudonym
# What are the disadvantages of introducing the IO monad?
Posted by: username_0 on 2023-07-09
The `IO` monad, or the *Karmic monad* as what it's called in the language I'm making, is a catch-all for every side-effects.

Introducing this monad has many benefits, mostly for encapsulating and monitoring side-effects, and for preventing security threats.

But what are its disadvantages? The major disadvantage I'm seeing is debugging issues. Unless something like Haskell's `Debug.Trace` is provided, it's impossible to see what's going on in pure operations. Are there any more crucial disadvantages?
**Tags:** monads side-effects
> **Comments:**
> **username_1**: There are really two questions here, one about making a language pure and one about using a particular mechanism of allowing side-effects in a pure language.  
---
## 3 answers
### Answer by username_7 on 2023-07-10. Score: 9 | Accepted answer
There are a few disadvantages, but they are really facets of one underlying design decision: the IO monad is a framework, and if there's one thing we know in the software business, it's that frameworks generally don't play nicely if you try to mix them.
As always, this is mostly subjective; one person's disadvantage is a price that someone else is willing to pay.
IO represents only one kind of procedure
Side-effects aren't the only reason why you might want "procedural" code. Another example is mutable data structures. So you'd like to be able to "invent" a procedural monad even if IO hasn't made it to this point in the code.
Haskell has the ST monad for this purpose. But now you have two kinds of procedural type: ST and IO, and it's not obvious how to mix them. There are ways to layer ST on top of IO (e.g. stToIO), and a module, Data.Array.MArray, whose only purpose is to abstract two different kinds of mutable array: the ST kind and the IO kind.
A third interesting kind of procedure is backtrackable destructive update, for use with backtracking nondeterminism. Here, you might have, say, mutable arrays, but mutations can be recorded and "undone" on update. I don't know if anyone has tried implementing this in a language like Haskell.
IO doesn't play nice with multi-threading
There are lots of ways to spawn threads in a language like Haskell, but they have one thing in common: they create a separate real world.
spawn :: IO a -> IO (IO a)

This has implications. For example, mutable data structures based on IO (e.g. IORef and IOArray) must be thread-safe by default, otherwise they are not safe. You can't pay for thread-safety in these primitives only when you want it.
Mixing monads and comonads is an unsolved problem
Some effectful operations (e.g. simulation) may be better expressed as comonads rather than monads. But IO requires monads, and we are yet to work out how to write code that uses both monads and comonads together in a useful way.
It may be that the problem is impossible in text, because "arrows" are inherently diagrammatic (see arrow syntax).
> **Comments:**
> **username_8** (2023-07-10): `STM` gives a form of backtrackable mutation. In logic-programming terms, `orElse` is a choice point, `retry` is failure, `TVar` is a logic variable, and the transaction record of an `atomically` block is the *trace* of instantiated logic variables that will be reset upon backtracking. As for comonadic & monadic programming, they *can* be mixed, using forms like `(Comonad w, Monad m) => w (a -> m b)`. However, you commit to an evaluation strategy based on where you put `w` & `m`, and sometimes you can’t use a particular `w` & `m` together, if one doesn’t distribute over the other.  
> **username_7** (2023-07-11): That's true, but in my head, `STM` is designed for short transactions, not for search. Specifically, in hardware transactional memory, the transaction can fail on a cache miss. And I'm certainly not saying that monads and comonads *can't* be mixed but rather, to quote the intro, they don't play nicely together.  
> **username_8** (2023-07-11): Right! Just making a couple observations, not trying to contradict you. I doubt STM would be *good* for search, but you made me notice a similarity I hadn’t before.  
> **username_7** (2023-07-11): Using transactions to implement search sounds like the infamous Prolog failure-driven loop. https://stackoverflow.com/questions/13057428/prolog-failure-driven-loops  
---
### Answer by username_2 on 2023-07-10. Score: 0
From what I understand of this topic, the "IO monad" was a solution for restoring a specific time-ordered evaluation to a language that did not in the first place guarantee to respect the sequence of evaluation specified by the programmer.

The reason a specific time-ordered evaluation is important with so-called "side effects" (in my view, as misnomered as suggesting drunkenness is a side effect of alcohol, when it is in fact usually the main effect), is because these side effects make changes to shared data in a way that the correct sequencing cannot be inferred purely by analysing the dependencies between the arguments and results of function calls.

Sometimes the correct sequencing cannot be inferred at all from just an analysis of the whole source code of the program - but can only be known by a programmer who is familiar with how the broader system works, and how the shared data is altered from outside the program in question.

So the existence of shared mutable data imposes a hard requirement that the programmer must, in at least some cases, be able to impose a specific order of evaluation which the compiler could not infer for itself as being the necessary order.

Languages like Haskell, meanwhile, do not guarantee to execute in the order the programmer specifies. They guarantee to execute only in the order which would satisfy the constraint that a function's arguments must be evaluated before it's results can possibly be computed.

In imperative programming, the problem is already solved because the program executes exactly as the programmer instructs.

The IO monad simply undoes (in a limited way) the consequences of not guaranteeing sequential execution in the first place.

There's no disadvantage to the existence of the IO monad, since it allows languages like Haskell to cope with scenarios which they otherwise could not cope with.
> **Comments:**
> **username_3** (2023-07-10): It's not that Haskell does not "*respect the sequence of evaluation specified by the programmer*", but rather the whole idea is that the sequence of evaluation is not important, no matter how you evaluate an expression you will always arrive at the same result. *That* is the guarantee that Haskell specifies, and it gives the compiler a great deal of freedom to optimise.  
> **username_2** (2023-07-10): @Bergi, agreed. The essential point is that the compiler assumes the right to evaluate in any order consistent with the explicit dependency of results upon arguments (or, I guess, any algebraically valid rearrangement of them). The issue with mutable shared data is that it is often shared external to the entire program (not just a particular function), and therefore the compiler often couldn't possibly possess the information necessary to determine a correct order - only the programmer does.  
---
### Answer by username_4 on 2023-07-10. Score: -1
Many people believe that monad is the thing that makes I/O possible in a pure functional language. But to my eyes it has only provided some high level constructs and syntactic sugar to make programming easier. The real thing to make I/O possible is ~~lazy evaluation~~ *something that is not used yet could have uncertain values, which is like lazy evaluation, and could be modeled as such, to hide the special properties in the language's own logic*, and the high level constructs have only hided the real thing from the user. (**Edit:** It's in the sense that a programmer could create something very similar to the I/O inside a program, for example to define coroutines and create I/O streams between coroutines, using a format unrelated to the built-in I/O features. That is, to play around with it, and extend it freely, instead of just using it. A language could make whatever exceptions for the real I/O, but if it is that case it doesn't make it interesting.)

If you want to add some more features, for example, to connect the input of something to its own output, and to allow closing and reopening a stream accessed from another coroutine (i.e. to make a copy of a coroutine with the stream changed and discard the original coroutine), it might be easier to firstly think in lazy evaluation, and only consider implementing it in monads.

Monads could make people notice some details hidden in most languages, but it doesn't tell you everything about them. In the two examples, while you already have monads, you may still need to insert the required interfaces somewhere by yourself, possibly using what is made available by lazy evaluation. In case you don't care about these details, you may just want I/O, instead of monad I/O.

If someone does everything right, I believe they could implement every feature of modern object oriented programming in a pure functional language, if we don't care about the time and space complexity. Saying we are implementing something using monads might be misleading, because that sounds like if we had monads, we had everything, while in reality, it is like saying we are implementing something using classes and objects, which doesn't really tell much about its complexity. I'd say monads are successful for modeling some common ideas, but won't credit everything to it.

In that case, there are no pros and cons, because the different programming paradigms would be effectively the same thing. But the problem is how much time you would want to invest in it, because the currently existed languages are obviously very far from achieving this. It's difficult to comment about the pros and cons because most people would stop somewhere before fully imitating object oriented programming, but I can't know exactly where.

---

The general idea is, the concept of monad itself doesn't capture everything happening in the background. It is something supposed to be possible to be defined in a functional language using its own syntax, not something in the infrastructure of the language itself. I may fail to describe what is really happening in the background well, but the point is that part cannot be ignored, or the I/O might become very restrictive.
> **Comments:**
> **username_1** (2023-07-10): The earliest versions of Haskell used nothing but lazy evaluation for doing "IO", but this was soon discovered to be very impractical.  
> **username_4** (2023-07-10): @leftaroundabout Any resources about the earliest versions? While it might be difficult to program in nothing but lazy evaluation, I don't think there is anything to prevent a programmer to define something monad-like by themselves.  
> **username_3** (2023-07-10): "*The real thing to make IO possible is lazy evaluation*" - I don't follow. Why wouldn't the IO monad work in an eagerly evaluated language? Especially since `IO` operations themselves are executed eagerly.  
> **username_5** (2023-07-10): When you say *"The real thing to make IO possible is lazy evaluation"*, are you referring to I/O in general or the `IO` type specifically?  
> **username_4** (2023-07-10): @DavidYoung Changed all to I/O to reduce confusion, though I might be still wrong or still not have expressed what I intended clearly.  
> **username_5** (2023-07-10): @user23013 I think might see part of the misunderstanding here. If I'm understanding you correctly, you are saying that the language needs some way to represent `IO` actions that are not (yet) executed (hence the initial things about laziness). This is so they can be combined into new actions without being executed right away. Is this roughly what you're saying? However, most languages already have this. In fact, most languages already have what Haskell specifically uses for this: functions (as values). You can combine functions before applying them (or or maybe you never apply them), etc.  
> **username_4** (2023-07-10): @Bergi Edited but I'm not sure I have clearly described what I intended. Speaking of the ideas behind the concepts, instead of the technicalities in a given implementation, the concept of lazy evaluation becomes somewhat subjective. Monads could hide lazy evaluation and use other ways to implement them, but I don't think it could retain all the good characteristics, but unfortunately I'm not able to list the potential good characteristics I'm thinking of.  
> **username_4** (2023-07-10): @DavidYoung Yes, this could usually be done just by using functions. It might be me having some misunderstanding about pure functional programming. In that case I might have used some wrong terms, but I can't think of another term. I think someone could also just use normal functions with side effects for I/O, where wrapping something in a function is most useful. But it's all about removing side effects.  
> **username_6** (2023-07-10): See https://stackoverflow.com/questions/17002119/haskell-pre-monadic-i-o. The link it provides to the Haskell 1.2 report is dead, but the report can also be found [here](https://dl.acm.org/doi/pdf/10.1145/130697.130699). (The formatting of that document is... different; a better copy may be available somewhere else.)  
> **username_4** (2023-07-12): @chepner Thanks. That's exactly what I expected it to be. But I don't see the part to make it supposedly impractical. I'm more interested in why they decided to move away. (For some reasons, I assumed it very long to read and only looked closely when I had time.)  
> **username_6** (2023-07-12): Within a single function, I think it would be simple enough to correctly pair items added to the `[Request]` list that `main` returns with the elements of the `[Response]` list that `main` receives as an argument. But as you start trying to spread I/O across multiple functions, maintaining the correct correspondance gets more difficult.  
---
