4 ms·
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
by chriswarbo 12y ago
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_and_elimination http://en.wikipedia.org/wiki/Natural_deduction#Introduction_...
[2] http://en.wikipedia.org/wiki/Church_encoding#Church_Booleans http://en.wikipedia.org/wiki/Church_encoding#Church_Booleans
[3] https://existentialtype.wordpress.com/2011/03/15/boolean-blindness/ https://existentialtype.wordpress.com/2011/03/15/boolean-bli...
- pron 12y agoI 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://www.lambdacs.com/debugger/, http://pleiad.dcc.uchile.cl/tod/download.html http://pleiad.dcc.uchile.cl/tod/download.html commercial: http://chrononsystems.com/ http://chrononsystems.com/, production time: https://www.takipi.com/ https://www.takipi.com/
- chriswarbo 12y ago