5 ms·
I don't really agree with your treatment of monads, but perhaps I don't understand your point of view. What did you mean by "you can't peek at it"? I can peek
by lincolnq 18y ago
I don't really agree with your treatment of monads, but perhaps I don't understand your point of view.
What did you mean by "you can't peek at it"? I can peek into the String returned by getLine in pure code, as in the following example:
myStr <- getLine
let len = length myStr
print len
Here, myStr is of type String and 'length' is a pure function of type [anything] -> Int. The <- ("gets") syntax unwraps the IO box around the result of getLine (which is of type IO String, as you noted). Once it's unwrapped, we can treat it as a purely functional value and call length on it. Then I can pass it to the 'print' function, which is also in the IO monad. The compiler will ensure that 'length' is pure.
I also don't understand what you mean when you say getLine "takes nothing as its input". Since it's in the IO monad, it implicitly takes the "RealWorld" as its input, updates it, and implicitly returns a new RealWorld. I prefer this treatment of the IO monad, because it's easy to see how _every function_ is pure in Haskell -- it's just a matter of giving it different (implicit or explicit) arguments.
To be fair, the RealWorld analogy is not exactly how the system works. But it is the abstraction that the Haskell language likes to make (from the docs: "RealWorld is deeply magical. It is primitive, but it is not unlifted (hence ptrArg). We never manipulate values of type RealWorld; it's only used in the type system, to parameterise State#.")
Anyway, an example which is actually purely functional but uses mutation:
import Control.Monad.State.Lazy
incr :: State Int ()
incr = modify (+1)
add2 :: Int -> Int
add2 val = execState incrTwice val
where
incrTwice = do
incr
incr
main :: IO ()
main = do
print (add2 5)
This program prints 7.
The 'incr' function here, like the getLine function, accepts no explicit arguments. However, it is in the State monad, parameterized with an Int, meaning that it implicitly accepts a mutable Int which it can modify. In this case, it adds one to that int, and doesn't return anything (that's the () data type, pronounced "unit").
The 'add2' function here is purely functional: it has type Int -> Int. And yet it depends on mutation occurring because it calls incr (twice). The execState "creates" a State monad from scratch and gives it an initial state (val), and then runs the given State action (incrTwice).
The main difference between the functional State monad and IO (which -- awesomely -- is actually defined in the source code as a State of RealWorld) is that you can't create instances of RealWorld, so you can't execState an IO action. In order to run an IO action, you have to get the RealWorld from the only place it enters your program, the main function.
Did I clarify anything, or did I misunderstand you?
- deleted 18y ago[deleted]
- deleted 18y ago[deleted]
- rkts 18y agoOk, you're right. That is a better explanation of monads than the one I gave. I intended my example to represent cases where you want real mutation, in the sense of modifying a data structure in place. Your code using the State monad, if I understand it correctly, just abstracts functional updates in a way that looks like mutation. Anyway, my intent in my original comment was simply to explain the practical consequences of pure functional programming. That, for instance, if you have a pure function that you want to randomize, this change must be reflected in all the code that depends on it. My intuition is that this is a bad thing, but I don't have enough experience to say for sure. However, it is apparent to me that purity 1. adds a lot of complexity to the language, and 2. has no clear practical benefits, which is why I say it's a waste of time.