I'm pretty sure monads can have side effects and be eager. It's just that not everything will be captured by the type signature. AFAIK this does not prevent it from being a monad. You can have monads without any type signature. Also, AFAIK the eagerness is a requirement due to the potential for errors changing program flow that is not captured by the type signature. But AFAIK this does not prevent it from being a monad.
AFAIK monads only have to conform to the monad laws, bind and return.
I'd like to know if I'm wrong because I'm concerned with the conflation of monads and language features of haskell. Specifically we can benefit from monadic composition without making javascript into haskell. I think Erik Meijer has been rather damaging in this regard. I know I'm in the minority so I'd like to be proven wrong so I can come join the party :)
I mean, we can talk about how it's pure in the sense that it just lifts other things in to being tied to IO programs, and it's our evaluation of them that causes mutation, but that's pedantic even to me and seems to miss the point.
That sense of purity isn't what most people mean or are concerned by.
However IO follows all of the Monadic laws, I believe the comments about side effects are focusing on the side effects that show through when you try and apply the monadic laws to promises.
So in JS notation:
{ action: {type: 'readFile', fileName: f}, next: content => mkPrintAction(content) }Regardless of your opinion on the matter, that's how it's actually implemented. IO is roughly equivalent to "State RealWorld#" with some extra strictness/unboxing stuff.
What you're talking about is basically the free monad over the (Request, Response -> a) functor. That would probably work, but be less performant and harder to extend.
IO is nice because it's really easy to safely integrate with FFI calls. I don't think you could easily do the same with your proposal.
The interpreter is simpler and easy to implement in any language too.
If you look at the generated core code, it's important to notice that it's not the same "token" being passed around everywhere. Each IO action returns a new token, which could be a different value from the old token. (Again, this is entirely theoretical, as the token/world state doesn't exist in the binary.)
Saying
> when you call "print", it returns a "new world" that is the state of the world after printing
makes no sense, since the word "state of the world" will be interpreted completely differently by the receiver of the explanation. State of the world = a large state object with lots of fields describing every single thing in some "World" (what world: program world? computer world? network world? all the world?). Thats what people hear.
The name of the type is horrible, and the world-passing intuition is also horrible. "Token" would've been much better, but its still not clear how to implement or why its even necessary (in a non-lazy language)
On the other hand, a tuple representing the current action and the function that will return the next action (or nil) is fairly clear, easy to implement an interpreter for, the pure/nonpure distinction becomes obvious (the runtime interpreter is the impure part), and laziness doesn't even need to get into the mix.
oh, and also regarding FFI: {type: 'FFICall', fn: string, arguments: [...]}
> {type: 'FFICall', fn: string, arguments: [...]}
I agree that this is simpler to understand, but it's no good in practice. It's obviously not type safe, and you still have to bake support into the interpreter/runtime for whatever your string value is. It's also effectively impossible to inline/optimize away. Which, again, is probably why it's done using "State RealWorld#" in GHC.
I would like to reiterate that however RealWorld# happens to be defined, the way its use is enforced in GHC is the exact same as if it did actually contain a description of the entire universe. You just aren't allowed to look at it. But, you are correct, this is more confusing than a request/response free monad.
The lift to constructing an IO program from a Haskell type is pure, but the execution of that program to generate a value or effect on the system is not.
So Haskell is "pure" when it comes to IO, but not in the way anyone except overly pedantic people mean the term.
> So the (a, World) result isnt fully deterministic from World. Even if you make each IO operation atomic, a chain of IO is still impure because the final (a, World) result depends on mutations introduced during execution.
It is fully deterministic from World but you cannot possibly describe a World so you can't supply it nor replicate it.
> So Haskell is "pure" when it comes to IO, but not in the way anyone except overly pedantic people mean the term.
You can't create a World so it is pedantic in that fashion but you are able to think of the functions as pure otherwise making integration easier.
A simple example is that IO happens in the correct order in Haskell because of the evaluation order not due to some external construct ensuring that lazy doesn't bite you.
Thats my best attempt, and it still sucks! I bet someone will start wondering about the uncertainty principle there... So lets just use a free monad or thunks in a strict language
function pureAction() {
return function() { return impureAction(); }
}
function chain(pureAction, fnReturningOtherAction) {
return function() {
var impureResult = pureAction();
var newAction = fnReturningOtherAction(impureResult);
return newAction();
}
}
function interpreter(mainAction) { return mainAction(); }
Everything is pure, since all the functions don't do anything but return thunks for the interpreter to run. Except the interpreter which actually runs those. This isn't even a theoretical explanation - its exactly how PureScript's Eff works.Promise.resolve(a).then(f) == f(a)
Immediately this is not true, since `f` can throw or return a non-promise.
But lets say we restrict the type of f so that it can only return promises. Once again, the statement is not correct - the executed side effect is not the same. The left side will execute with one tick delay of the microtask queue, while on the right side the side effect executes immediately. So the side effects are not equivalent. If we write `f(a); f(b);` and `Promise.resolve(a).then(f); f(b);`, in the first case the side effects will start in order; in the second case, they will start in the reverse order. If we cannot replace one side of equal sign in the law with the other and have things run exactly the same, that means equational reasoning doesnt work [1], and the law doesn't apply.
In theory you could probably make monads with side-effects, but promises don't satisfy the laws. Its much easier if the monad only represents side effects but doesn't actually run them. You can think of them as redux action objects - they don't actually do anything once they are created. They only represent the side effect that will be run. For example, their internal representation could be {action: 'readFile', argument: 'path/to/file.json'}. ($)
You can construct actions from other actions by chaining them with a function that will take the result from the action and produce a new action object. Those would be derived actions. You can think of them as cons cells - they have the original action, and the function that produces a new action. Example representation: {originalAction: {action: 'readFile', argument: 'path/to/file.json'}, nextActionFunction: result => printLine(result)}
Finally, you pass the action to the "interpreter". The interpreter reads each action, runs the originalAction part of the cons cell, then produces a new actioin using the function in the nextActionFunction cell. It repeats that until it gets to an empty nextActionFunction cell.
($) Anoter practical representation for compile-to-js languages would be to use a thunk for the base (original) action. A thunk is a function without arguments, that when run, will produce a side-effect and return a value. The interpreter would then be simply a thunk runner. An async thunk can also be used, and thats a thunk that takes a callback that will be called with the eventual value. Still, we're only producing values without creating side effects: the laws are much easier to uphold.
[1]: http://www.haskellforall.com/2013/12/equational-reasoning.ht...
Edit: And with regard to the side effect ordering question -monads are only well-known defined for pure functions anyway! By that standard, any Scala code using non-pure functions is also not using a monad. This I inevitable in a language that does not enforce purity, which is basically every widely-adopted one that's not Haskell. A definition of "support" that excludes implementations that allow (but do not require) non-pure functions is not a very useful definition.
And if you can't use the laws for equational reasoning, then what good is it whether its a "monad" or not anyway?
At that time, monadic tasks were basically unheard of in the community. By the very nature of the monad laws, they cannot have side-effects, so they cannot be eager. This means that a user "running" an operation would observe nothing until they subscribe to it. In an imperative language like JS, this is confusing for users, and arguably not worth the breakage of the existing promise ecosystem.
Another nice property of Promises is that they can be implemented in a way that doesn't cause stack overflow for endeless recursion (and additionally, in some cases we can get "tail recursion"). This might be doable with lazy tasks too - not entirely sure. See https://github.com/paf31/purescript-safely
On the other hand, monadic tasks make many other things easier, like
* cancellation (just unsubscribe),
* synchronous error propagation, so no issues with error swallowing and perhaps no post-mortem issues in node. Nothing runs before the subscription, so at the point the side effects are running, we already know whether there is an error handler. Effectively the `done` method would've been the eqiuivalent of `subscribe`, and mandatory
* defining parallel, sequential or limited-concurrency execution order after the tasks have been created