4 ms·
IO a is short for roughly World -> (a, World) and is thus computationally pure. You cannot figure out what happens to the World in between but you don't need to
by Guvante 10y ago
IO a is short for roughly World -> (a, World) and is thus computationally pure. You cannot figure out what happens to the World in between but you don't need to, thus the side effects "don't exist" from a analysis standpoint.
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.
- zeahfj 10y agoWould it be fair to say that the IO Monad get passed along implicitly with the other Monads in an eager evaluating system? Would this make it a stack? Just one that's not represented or enforced by the type system.
- Guvante 10y agoI dunno, so much of the IO Monad is specific to Haskell and lazy evaluation. Generally I think of it as Haskell's quirky way of handling the real world in a pure lazy mathematical construct and not something fundamental.
- spion 10y agoThe "world" equivalence is not a useful way to think about it. A more useful description would be a tuple that contains two items, the first one being the description of the action to execute, and the second one being the function that will be called with the value retrieved with the action executed. So in JS notation: { action: {type: 'readFile', fileName: f}, next: content => mkPrintAction(content) }
- wyager 10y ago> The "world" equivalence is not a useful way to think about it. 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.
- spion 10y agoIts just that when you mention this, the assumption is that "World" is the state of the world. Which is not true, since world is just a token to impose ordering of execution in a lazy language, but its a fake state token since its not present at run time... and that complicates the explanation further. The interpreter is simpler and easy to implement in any language too.
- wyager 10y agoThat's true, the compiler does optimize away the explicit dependency on the world. But it still makes sense; when you call "print", it returns a "new world" that is the state of the world after printing. You can't inspect the state of the world, so it doesn't really matter whether it's just a token or an actual description of the universe. The behavior is indistinguishable either way.
- spion 10y agoThat assumes there is only one program running in the world. Its simply not clear what the "world" type means. Its a bad name. I'd rather go ahead and explain it as a token.
- wyager 10y agoI don't see why it assumes there is only one program. If the world value is impossible to inspect (which it is), you can assign whatever semantics you like to it. 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.)
- spion 10y agoOk, I'll put it this way: 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: [...]}
- anewhnaccount 10y agoHonestly I've always just thought of Monads as-in Haskell as a tricky way of sequencing in the presence of lazy evaluation. Looked at this way, the "trick" is to make a data dependency between each step in the form of an argument. Since Haskell names must be bound before they're read this means we can guarantee the order in which the steps are evaluated relative to one another. Then the whole thing is wrapped up in a data structure (this latter part seems to be what people normally focus on).
- SomeStupidPoint 10y agoThis is a late reply, but your abstraction is leaky: World might mutate during an IO operation in a way which impacts the outcome. 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. 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.
- Guvante 10y agoWorld cannot be inspected in any meaningful way in this paradigm so it is fully deterministic because the differences aren't important. > 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.
- spion 10y agoWhich only goes to show that you can't apply real world reasoning to explain an academic concept. In this purely theoretical concept, World (which cannot be inspected) does contain the entirety of the world, including all other actions running in parallel, the current time and the state of every atom in the universe. So given the exact same state of the universe and assuming purely deterministic laws governing it (or the state of the world containing the future of the world too), yes, the action will produce the exact same output, because all other actions running in parallel or the things that will lead to the future actions are also part of that input World state. 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.