3 ms·
I think you're right in that ghc's actual implementation is something like that, but it is a bit lacking in describing the semantics. To see why, imagine functi
by zenhack 8y ago
I think you're right in that ghc's actual implementation is something like that, but it is a bit lacking in describing the semantics. To see why, imagine functions:
getLine :: RealWorld -> (String, RealWorld)
putLine :: RealWorld -> String -> RealWorld
And some code like:
Let (line, w1) = getLine w0
w2 = putLine w1 line
w3 = putLine w2 "other stuff"
w4 = putLine w3 line
...
Now, imagine an optimization compiler for some reason decides it would be better to recompute `line`, rather than hang on to the value in the interim. It would be well within its rights to do so, because referential transparency guarantees that this is safe.
IIRC ghc essentially "plays dumb" and just never duplicates a computation (which is not typically a desirable optimization at this level, though register allocators will sometimes do it). But it's arguably a hack.
A better metaphor is to think of values of type IO a as fragments of a script in some other (imperative) language. The Haskell program merely computes the script, and the runtime then takes care of actually executing it. In this sense, effects in Haskell are not "side" -- they are first class citizens. What order the fragments of the imperative program are computed in (or how many times) is independent of the text of the final program.
In very early versions of Haskell, main had a type roughly like [Response] -> [Request]; you computed a list of things for the runtime to do, as a function of the results of its (hopefully) previous commands. This kinda works, but it's easy to deadlock by creating a circular data dependency. What the Monad structure does for you is avoid that problem, by making it impossible to depend on a future value.
It's also worth pointing out that all of this is specific to IO; understanding monads in general are neither necessary nor sufficient to understand Haskell's IO.
- clementi128 8y agoThanks to all for the answers! I want to make sure that I'm understanding this bit correctly: >A better metaphor is to think of values of type IO a as fragments of a script in some other (imperative) language. The Haskell program merely computes the script, and the runtime then takes care of actually executing it. In this sense, effects in Haskell are not "side" -- they are first class citizens. What order the fragments of the imperative program are computed in (or how many times) is independent of the text of the final program. In Wadler's paper, the monadic output example looks like this: type M a = (Output, a) type Output = String unit :: a → M a unit a = (“ ”, a) (*) :: M a → (a → M b) → M b m * k = let(x, a) = m in let(y, b) = k a in (x ++ y, b) out :: Output → M () out x = (x, ()) where each function, instead of returning a simple numeric value, returns a tuple of a string (the output to be printed) and a number (the value to be returned). If you wrote this function out in Haskell, you would have access the output text in the form of a lazy list of strings. If we can think of IO values as fragments of an imperative script that are handled at runtime, then that makes me think that the Haskell compiler doesn't use the strategy presented in Wadler's paper (in the case of printing output, returning a lazy list of strings to be printed), and instead just performs the specified side effects directly, knowing that semantics of the IO monad guarantee that things will be sequenced correctly. Do I have this right?
- lobster_johnson 8y agoRight, IO does not collect the results this way. putStr for strings is declared like so: putStr :: String -> IO () Here, () is the unit type, which is similar to null/nil in other languages. In other words, I/O functions like putStr etc. don't return anything. putStr internally calls lower-level functions that actually write to a file descriptor. Some people get into Haskell thinking that functions can't have side effects because Haskell is "pure", but that's not right. Rather, as the parent commenter say, they can have all the side effects they want, and the consistency and purity of the program's execution is preserved through the use of monads. Haskell is considered a pure functional language where everything is immutable, but that's not true, either. A lot of the GHC standard library uses mutation (e.g. with IORefs) for performance reasons. For example, putStr ends up writing to an internal byte buffer that is modified directly.
- zenhack 8y agoMore or less, yeah. Most of the language-aware optimization in ghc happens in an intermediate form that still carries the types, and then ultimately it spits out code that just has side effects as a result of evaluation like every other language. The code you sketch out above is basically the Writer Monad, which exists in the libraries, and does see use, but it's not IO -- as I mentioned above the semantics of IO don't entirely just fall out of the monadic structure. But I find the computed-imperative-program description lends itself to thinking about IO values as values in a way that thinking about them as type system goop to tame side effects doesn't; for example, the program: import Data.List (intersperse) import Control.Concurrent (threadDelay) main = sequence_ $ intersperse (threadDelay 1000000) (map print [1..]) will print out the natural numbers, one per second until you kill it. It works by constructing a (lazily evaluated) list of actions to perform, which alternately print out a number or wait for one second. sequence_ has type [IO a] -> IO (), and it just performs each action in the list in sequence (its type is actually a bit more general than this, but..). This kind of code is really weird to think about from the "tamed side-effects" model, but very natural from the "computing a script" model.