Don Klein
Don Klein

Reputation: 215

Curious about why get in StateT monad transformer returns a instead of (a, s)

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

Answers (1)

chi
chi

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:

  • First, we read the old state of type s. This is the s -> .. part in the type s -> m (a, s).
  • Then, we run some action in the monad m. This is the .. -> m .. part in the type s -> m (a, s).
    • The monadic action returns a new state, to replace the old one. This is the .. -> .. (.., s) part in the type s -> m (a, s).
    • Finally, monadic action also returns a value, of a possibly different type 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

Related Questions