Reputation: 215
I was reading the StateT monad transformer's source, as it reads like this:
get :: (Monad m) => StateT s m s
get = state $ \ s -> (s, s)
I expanded the above code by switching out state
and got this, but still fail to see why it's not returning a tuple.
a <- StateT (return . (\ s -> (s, s)))
From the code above it seemed get
returns a tuple (s, s)
, which looked fine, but I'm wondering why when using it, get
returned an Int
, instead of (Int, Int)
?
I traced a lot of source code trying to find when or what changed it but to no avail.
w :: StateT Int IO String
w = do
a <- get
liftIO $ print a -- 2, but why? shouldn't this be (2, 2) instead?
return $ show a
result = runStateT w 2 -- ("2",2)
Upvotes: 2
Views: 140
Reputation: 116174
A value of type StateT s m a
, modulo newtype
wrappers, is a function with type s -> m (a, s)
.
The function (return . (\ s -> (s, s)))
has type s -> m (s, s)
, hence once it is wrapped by the StateT
constructor becomes a value of type StateT s m s
.
Note that a value of type StateT s m (s, s)
would instead involve a function of type s -> m (s, (s, s))
which is not what we are using here.
Your confusion seem to arise from the "other" s
in m (s, s)
, which does not contribute to x
when we run x <- get
. To understand why, it's useful to think to think to what a stateful computation performs:
s
. This is the s -> ..
part in the type s -> m (a, s)
.m
. This is the .. -> m ..
part in the type s -> m (a, s)
.
.. -> .. (.., s)
part in the type s -> m (a, s)
.a
. This the .. -> .. (a, ..)
part in the type s -> m (a, s)
.Running x <- action
handles all these types automatically for us, and lets x
to have the result type a
, only.
Concretely, consider this imperative pseudo-code:
global n: int
def foo():
if n > 5:
print ">5"
n = 8
return "hello"
else:
print "not >5"
n = 10
return "greetings"
In an imperative language, we would type this as foo(): string
, since it returns a string, disregarding its side effects to the global n: int
and the printed messages.
In Haskell, we would instead model that using a more precise type like
Int -> IO (String, Int)
^-- the old n
^-- the printed stuff
^-- the returned string
^-- the new n
Again, running x <- foo()
we want x: string
, not x: (string, int)
, following what would happen in an imperative language.
If instead we had a function
global n: int
def bar():
old_n = n
n = n + 5
return old_n
we would use the type
Int -> IO (Int, Int)
since the returned value is an Int
now. Similarly,
global n: int
def get():
return n
could use the same type
Int -> IO (Int, Int)
Now, one might argue that the second Int
is not strictly necessary here, since get()
is not really producing a new state -- it's not changing the value of n
. Still, it's convenient to use a type of the same form, s -> m (a, s)
, as any function which could change the state. This is so that it can be used together with any other function having a similar type.
Upvotes: 3