3 ms·
But isn't it the case in any programming language that impure operations only have side effects when they are executed? You might as well argue that a JavaScrip
by goto11 5y ago
But isn't it the case in any programming language that impure operations only have side effects when they are executed? You might as well argue that a JavaScript program on its own is pure - it is only when it is executed by the runtime it has side effects.
You can also pass a function around without executing it in JavaScript.
- pwm 5y agoA good mental model is that Haskell has expressions that are evaluated and JavaScript has statements that are executed. const io_actions = ["hello", "world"].map(s => console.log(s)); io_actions[1]; vs. io_actions = map putStrLn ["hello", "world"] main = io_actions !! 1 JS will print out "hello" and "world" and the value of io_actions[0] is undefined. In Haskell, the value of io_actions !! 0 is IO "world" which is just a value you can pass around in your program. Only when the Haskell runtime evaluates your code is when it will do the effect, which is to print "world". "hello" is never printed. Naturally you can also pattern match on IO (): f :: IO () f = case putStrLn "hello" of _ -> putStrLn "world" also only prints "world". The IO type in Haskell is akin to IO (State World -> (State World, a)), ie. the state monad where the state threaded through the computation is the "real world". This can be demonstrated with the following: f :: IO () f = IO $ \world -> case putStrLn "foo" of IO _ -> case putStrLn "bar" of IO g -> g world prints "bar".
- goto11 5y agoThe JavaScript equivalent would be: const io_actions = ["hello", "world"].map(s => () => console.log(s)); io_actions[1](); which also prints "world" when the JavaScript runtime evaluates the code.
- pwm 5y agoIf your point is that one can implement a Haskell like language in any turing complete programming language then sure, you are correct. GP's point however was that Haskell is purposefully designed to be written and thought about this way from the ground up. Ie. putStrLn "hello" is not a command that is executed verbatim when the runtime gets to its source position. It's an IO action and an IO action is just a value that can be passed around or, by virtue of IO being a Functor, mapped over, etc... If you say that purity and referential transparency are not only reserved for Haskell then again technically sure, but the point of Haskell is that it makes these concepts the centre of its design. Instead of having to jump through hoops to write code where you can do the above you have to jump through hoops to do the opposite.
- goto11 5y agoMy point is just that input/output is not pure in Haskell. I believe it is an unfortunate misconception that obscures what the actual cool properties of Haskell is. IO functions like putStrLn is curried by default, so you can pass them around without executing them. But this does not affect the purity of executing them. And it is not something special for Haskell, you can pass functions around without executing them in almost any language. The cool thing about Haskell is that code is pure by default, and it is only due to special-casing by the language (the type of "main" being "IO ()") that you can even have operations with side-effects. Haskell allows you to have parts of the program guaranteed pure, but the program is a whole is not pure if it does any input/output.
- przemo_li 5y agoIO is also pure. It will not be executed until the very last line of your app. O_O No seriously. If you wanted to model IO in say Java. Your whole program would be about constructing values. No I/O what so ever. Only do this then that then get a, then if a is this do x other..... Whole Java app and I/O would only happen when at the very end you execute run method on that constructed value. That'd not usual Java. After all in Java code itself can do control flow and I/O. Do why bother with those values. Need I/O on line ten out of 100. Do I/O immediately on line 10. What I/O on Haskell is is side effect. Explicit side effect. It's possible to write impure code in Haskell but that is not IO. It's when you use escape hatches that break type system guarantees.
- Twisol 5y ago> But isn't it the case in any programming language that impure operations only have side effects when they are executed? That's a great question! This framing is indeed universally true, which suggests there's something less useful about one or more of the definitions it relies on. For my money, I'm betting on our definitions of "executed" being different. What exactly does it mean to execute a program? For most of us practitioners, it means using a program for its intended purpose. I think this is closest to the meaning you're using. But this embodies everything at all about a program, so it is quite coarse grained. Can we break "execution" into smaller pieces? Yes, at least conceptually. First, we can speak of "reduction": it's what happens when you simplify "1 + 1". Second, we can speak of "interaction": it's what happens when you substitute for "x" in "x + 1". Reduction is purely internal: the program state evolves purely on the basis of the information it already has. We perform reduction in order to turn the program into a more useful form, much as in algebra we might put a linear expression into slope-intercept form. Interaction operates explicitly on the exchange of information between two parties: the program and its environment. We perform interaction when we need to make some information known by one party available to the other. Personally, I take my definition of "side-effect" to mean "requires interaction". For instance, take the (JavaScript) program `(x) => x + 2`. This program cannot be reduced; in order to reduce it further, we need to interact with it by providing an argument. That interaction transforms the program to, say, `((x) => x + 2)(42)`, and now we can reduce down to `44`, which cannot reduce and cannot interact. Of course, if my program was `((x) => x + 2)(42)` from the beginning, there is no side-effect. At least, not one visible from the outside: the interaction between programs `(x) => x + 2` and `42` has been promoted to a reduction via composition. This kind of interaction allows us to implement request-response protocols between the program and an environment, so long as we have a runtime to mediate the interaction. But it's also somewhat painful to author programs in this style: we have to return a callback all the way back to the top of the program whenever we want to interact. If we add interaction primitives to the language itself, as in `prompt("Enter a number:")`, we gain the ability to interact from anywhere in the program, in exchange for the added complexity of a runtime that has to keep track of where in the program the interaction is occurring. This is great! But can we promote the `prompt` interaction to an internal reduction as we did with function application? Well, in JavaScript, _kind of_. Since most symbols are dynamically looked up, you could overwrite `window.prompt` before running the inner program, and reset it afterward. But this impacts every call to `prompt`, not just the one (out of potentially many) you wanted to intercept. Providing an internal `prompt` interaction becomes a bit fraught; it's not very composable. And in other languages, you couldn't do this at all. (Dynamic lookup is itself a kind of primitive effect system, so you can backdoor a surprising number of things through it.) Suppose that, instead of adding interaction primitives directly to the language, we stick with the original idea of "a returned top-level function is a request for interaction", but try to dress it up a bit more nicely. We still want to be able to perform interaction deep within the program, but we need some out-of-the-way machinery that propagates that interaction up to the top level for us, and manages remembering how to get back to where we were. In other words, we need to implement the system that was previously in the runtime in our program instead. Only the actual doing of actions is left in the runtime. Without burying the lede, if you do this you end up in one of a few algebraic systems, one of which is monads, and all of which have the property that you can promote interaction to reduction in a composable fashion. (With monads, you need to define your own monad capturing the interactions you care about, and then write an interpreter for that monad that turns your monad's interactions into IO interactions.) Basically, you can mock out the environment in a particularly clean way. Monads end up being useful for quite a lot besides managing IO. It's very appealing as a solution that doesn't seem to be special-cased for IO specifically. I think that has a lot to do with how they allow you to in general program against interactions and then promote to reductions. It's harder to do that in JavaScript, since there are so many effectful operations, and while you can promote some of them, you can't promote all of them.