Hacker News new | past | comments | ask | show | jobs | submit login

Consider an analogy to booleans. We can get a boolean in many different ways ('true'/'false' constants, numeric comparisons, etc.). We can only use a boolean in one way: selecting the branch of an if/then/else construct. All logical connectives, (de)serialisers, conversions, etc. and everything other 'users' of booleans that a language may have built-in can all be replaced using combinations of if/then/else. More formally, we could say that booleans have many introduction rules but only one elimination rule (if/then/else)[1].

We can easily remove booleans from a language by combining introduction and elimination into one step. In other words, we can replace introduction/elimination combos like this:

    bool b = lessThan(x, y);
    if (b) { foo; } else { bar; }
With a single construct, like this:

    ifLessThan(x, y) { foo; } else { bar; }
We can do this to every boolean introduction rule; in particular, if we do it to the constants 'true' and 'false' we end up with a form of the "Church booleans"[2]:

    ifTrue  { i_will_always_run; } else { i_will_never_run;  }

    ifFalse { i_will_never_run;  } else { i_will_always_run; }
If we think about first-order programs, what effect does this transformation have? It brings together the reason for a decision with the consequences of that decision. In other words, to use 'ifLessThan' we must have the numbers ('x' 'y') which 'do the choosing' and the branches ('foo' 'bar') which get chosen, at the same time.

What if we inspect the stacks of these programs? They will always show us the reason for branches being taken! We're never just in the branch of an 'if' construct: we're in the branch of an 'ifLessThan' construct, and we can see what the numbers were; or an 'ifEqual' branch; or an 'ifPhaseOfTheMoon' branch; or whatever. In other words, to paraphrase your complaint about monadic IO, booleans erase all context!

In a language with booleans, we can write a complex, deep calculation which returns a boolean; then we can use that boolean elsewhere. Reasoning about such programs is incredibly difficult, since all of the stack information built up during our complex calculation is thrown away once it returns; by the time we hit a problem using the boolean, we know nothing about it other than 'true'/'false' (this is also related to the idea of "boolean blindness"[3]).

This same argument can actually be applied to every data type (numbers, lists, etc.).

So, why do languages use booleans? Because despite these problems, this ability to split up reasons from consequences can be very useful too! It lets reify decisions into concrete values. We can manipulate these values with an intuitive, high-level algebra (AND/OR/NOT/etc.). It lets us separate how we make decisions from what those decisions are. As long as these decisions are given descriptive variable names like "queue_is_empty", and we avoid the temptation to collapse everything together with logical connectives, then we can still reason effectively: "we got here because the queue was empty".

In a language without booleans, we can regain this power using higher-order functions (passing branches around as variables); or we can write first-order boilerplate to model the same thing. But why bother when we can just use booleans?

Exactly the same argument can be applied to reified ('monadic') IO. The core idea is to represent IO actions as data, just like booleans represent decisions. Just like with booleans, this lets us separate how we decide on actions from what those actions are. Likewise, we can use (more or less) intuitive, high-level algebras to manipulate these values. Monads are one example of an algebra for doing this; applicatives, effects and arrows are other algebras we could use instead. Note that we can also manipulate boolean values using algebras other than "boolean algebra"; eg. as a ring.

Yes, we lose the context of what caused an action to be decided on; but it's exactly the same as losing the context of what caused a boolean to be true/false. The "solution" is the same in both cases too: look at how it was produced, not how it's used.

Booleans are either "true" or "false", not matter which operations went into their construction. We can't 'inspect' a boolean to recover whether it 'contains' 'AND's/'OR's/'other booleans', etc. Likewise, trying to 'inspect' an IO action to recover whether it 'contains' 'PrintLn's/'open's/etc. is just as futile and defeats the point of IO actions.

In other words, if you're recreating/rebuilding things to work with IO actions then you're DoingItWrong(TM). Trying to, for example, look for a "println" call 'inside' an IO action is like trying to look for an "OR(x < 5, ...)" call 'inside' a boolean.

[1] http://en.wikipedia.org/wiki/Natural_deduction#Introduction_... [2] http://en.wikipedia.org/wiki/Church_encoding#Church_Booleans [3] https://existentialtype.wordpress.com/2011/03/15/boolean-bli...




I understand your sentiment, and while it may apply to languages such as Haskell, it doesn't apply to languages running on such extremely flexible platforms, like the JVM. On the JVM (and not just, of course), you can represent any sequence of IO operations to be taken as an unstarted (or even blocked) thread. Then, you can use the excellent bytecode manipulation tools to transform that code. Doing so isn't easy, because it's not meant to be -- it's cleverness that should be limited to experts, and hidden from the language as an extra-linguistic tool.

> Trying to, for example, look for a "println" call 'inside' an IO action is like trying to look for an "OR(x < 5, ...)" call 'inside' a boolean.

Perhaps, but they're both extremely useful, and luckily, if you keep to the very powerful imperative abstractions provided by the JVM, you can do both:

> Reasoning about such programs is incredibly difficult, since all of the stack information built up during our complex calculation is thrown away once it returns

On the JVM, it's quite easy to transform any computation to record all state-changes and decisions (AKA "omniscient debugging")[1]. Of course, using monads will make things that much harder, because now you have to record not only a very elaborate object graph, but one that is detached from a thread. Production-time omniscient debugging tools can't do that.

> this lets us separate how we decide on actions from what those actions are.

Oh, absolutely; it's a terrific abstraction. Now all that's left to do is weigh the cost of the benefits of this abstraction against its cost.

The costs include erasing context and making post-hoc reasoning hard (both in debugging and profiling); also, this abstraction is infectious -- so it's hard to limit it to just the places where you need it -- and incompatible with code that doesn't use it. The advantage is manipulation IO operations much more easily that with bytecode transformers (as the JVM already treats all code as data, and makes it available for inspection and manipulation).

In short, this beautiful abstraction lets you do something that can already be done on all code on the JVM, but more easily, except it's limited to code that actually uses it, which makes it incompatible with code that doesn't (i.e. almost all code).

Why would I pay so dearly to do something I can already do much more generally?

[1]: Open source: http://www.lambdacs.com/debugger/, http://pleiad.dcc.uchile.cl/tod/download.html commercial: http://chrononsystems.com/, production time: https://www.takipi.com/


> Why would I pay so dearly to do something I can already do much more generally?

For the same reason you might pay to use a typed language, when an untyped language is more general. Or why you might pay for encapsulation, when globals are more general. Or why you might pay for structured programming, when GOTOs are more general. Or why you might pay for a VM, when machine code is more general. And so on.

I also don't see why it's difficult to mix and match monadic/non-monadic IO. We can convert back and forth easily: "return" turns a non-IO value into an IO action and "run" (AKA "unsafePerformIO") turns an IO action into a regular value. We can think of it as just delaying and forcing function calls; in the same way that we can think of booleans as their Church encodings.

What's more, in an imperative context like the JVM, everything is already in IO by default, so we never need to do any conversion! Nothing becomes 'incompatible', since the JVM lets us perform side-effects whenever we like.

I think the more important distinction between the JVM and IO a-la Haskell is the laziness in Haskell. Without laziness, it would be awkward for a JVM language to abstract this stuff; in the same way that it's awkward to write ifThenElse as a function, or to write short-circuiting boolean operators.


> For the same reason you might pay to use a typed language, when an untyped language is more general. Or why you might pay for encapsulation, when globals are more general. Or why you might pay for structured programming, when GOTOs are more general. Or why you might pay for a VM, when machine code is more general. And so on.

Not all abstractions are created equal, and while all the other ones you mention do have some costs, they are dwarfed by the benefits; they're bargains. This one? I'd say it's overpriced junk (though it's pretty junk). The expressiveness it buys on top of what's already there (threads) is negligible, and it's very pricey.

Not only do abstractions differ in benefits and costs in general, those costs differ considerably depending on the platform.

> We can think of it as just delaying and forcing function calls

Delaying a "plain IO" operation is no different (in fact, it's identical) to just using a thread. The abstraction is already built in. The laziness of Haskell is a dual to (blocking) threads, and threads are already well supported. There's no need to replace them, especially if doing so severely harms your ability for posthoc reasoning (I'd say that even Haskell sacrifices posthoc reasoning for apriori reasoning, which is a wrong choice for most uses; but that's doubly worse on the JVM, which already has a lot of powerful tooling in place for working imperatively).




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: