I decided to simplify my code to see what conditions produced the error. I start with a simple nested "s" similar to ST s (STArray s x y)
like so:
{-# LANGUAGE RankNTypes #-}
import Control.Monad.ST
import Control.Applicative
data Foo s = Foo { foo::Bool }
newFoo :: ST s (Foo s)
newFoo = return $ Foo False
To test the state code, I run the following transformation:
changeFoo :: (forall s. ST s (Foo s)) -> ST s (Foo s)
changeFoo sf = do
f <- sf
let f' = Foo (not $ foo f)
return f'
I would like to extract a value from the state while keeping the state, so the next step is to extract the Bool value:
splitChangeFoo :: (forall s. ST s (Foo s)) -> ST s (Bool,(Foo s))
splitChangeFoo sf = do
f <- changeFoo sf
let b = foo f
return (b,f)
In order to extract that Bool I must use runST
. My understanding is that this will create an additional state, which I specify by providing a forall s.
in the return type:
extractFoo :: (forall s. ST s (Bool,(Foo s))) -> (forall s. (Bool,ST s ((Foo s))))
extractFoo sbf = (runST $ fst <$> sbf,snd <$> sbf)
The example above does compile without the final forall
however in the circumstance I am trying to debug this is not possible. Regardless, in this case it compiles either way.
I am able to use the above code to chain multiple uses of the state together:
testFoo :: (Bool, ST s (Foo s))
testFoo = (b && b',sf')
where
(b,sf) = extractFoo $ splitChangeFoo newFoo
(b',sf') = extractFoo $ splitChangeFoo sf
Now I try to throw IO into the mix and so I make use of the applicative fmap <$>
. This does not compile: (NB. same problem if I use fmap
or >>= return
rather than <$>
)
testBar :: IO (Bool, ST s (Foo s))
testBar = (\(b,sf) -> extractFoo $ splitChangeFoo sf) <$> testBar'
where
testBar' :: IO (Bool, ST s (Foo s))
testBar' = return $ extractFoo $ splitChangeFoo newFoo
With the following error:
Couldn't match type `s0' with `s2'
because type variable `s2' would escape its scope
This (rigid, skolem) type variable is bound by
a type expected by the context: ST s2 (Foo s2)
The following variables have types that mention s0
sf :: ST s0 (Foo s0) (bound at src\Tests.hs:132:16)
Expected type: ST s2 (Foo s2)
Actual type: ST s0 (Foo s0)
In the first argument of `splitChangeFoo', namely `sf'
In the second argument of `($)', namely `splitChangeFoo sf'
In the expression: extractFoo $ splitChangeFoo sf
I'm aware from this SO question about ST function composition that not all possible uses of ST are accounted for by the compiler. To test this assumption I modified the above function to not use IO and simply pass the return value into a lambda:
testFooBar :: (Bool, ST s (Foo s))
testFooBar = (\(b,sf) -> extractFoo $ splitChangeFoo sf) testFooBar'
where
testFooBar' :: (Bool, ST s (Foo s))
testFooBar' = extractFoo $ splitChangeFoo newFoo
Predictably this also does not compile with an identical error message.
This presents a challenge. I have a reasonable amount of IO that needs to interact with a set of deeply nested ST operations. It works perfectly fine for a single iteration, however I am not able to make further use of the return value.
I'm not sure how all of this works out in your actual code, and it seems to me that you're doing something more convoluted than strictly necessary, but as I can't say much definitive about that I'll only address the immediate problems in your question.
testBar
doesn't workIn (\(b,sf) -> extractFoo $ splitChangeFoo sf) <$> testBar'
, we have <$>
, which has type forall a b. (a -> b) -> IO a -> IO b
. However, testBar
has type forall s. IO (Bool, ST s (Foo s))
. We can only apply fmap
to testBar
after the s
there has been instantiated.
In your case, s
is instantiated to a fresh variable s0
, so inside the lambda sf
also has that parameter. Unfortunately, s0
is distinct from the topmost s
parameter (which we'd like to see in the return type), so sf
is pretty much unusable now.
In the case of testFooBar
, the issue is that lambda arguments are monomorphic by default (or else type inference would become undecidable), so we just have to annotate the type:
testFooBar :: (Bool, ST s (Foo s))
testFooBar =
(\(x :: forall s. (Bool, (ST s (Foo s)))) -> extractFoo $ splitChangeFoo $ snd x) testFooBar'
where
testFooBar' :: (Bool, ST s (Foo s))
testFooBar' = extractFoo $ splitChangeFoo newFoo
All we've done here is to introduce an unwieldy alternative for let
and where
expressions. But this has little significance, since in testFoo
the fmap
necessitates that the lambda argument has a fully instantiated type, so there we can't annotate our woes away.
We might try the following:
{-# LANGUAGE ScopedTypeVariables #-}
testBar :: forall s. IO (Bool, ST s (Foo s))
testBar = (\(b,sf) -> extractFoo $ splitChangeFoo sf) <$> testBar'
where
testBar' :: IO (Bool, ST s (Foo s))
testBar' = return $ extractFoo $ splitChangeFoo newFoo
Now testBar
has the right s
parameter to begin with; since it's not generalized anymore, we can apply fmap
to it right away. Inside the lambda, sf
has type ST s (Foo s)
. But this also doesn't work out, since extractFoo
expects a forall
quantified argument, and sf
is monomorphic. Now - if you try this version in GHCi - the error message we get is no longer about escaping skolems, but a good old-fashioned can't match type s with s2
.
Clearly, extract
must return a forall s.
-ed ST s (Foo s)
somehow. Your original type is no good because GHC doesn't have polymorphic return types, so
(forall s. ST s (Bool, Foo s)) -> (forall s. (Bool, ST s (Foo s)))
actually means
forall s. (forall s. ST s (Bool, Foo s)) -> (Bool, ST s (Foo s))
since GHC floats out all forall
-s to the top of the scope. This makes type inference easier and allows more terms to typecheck. Indeed, you can enter :t extract
in GHCi and be presented with the floated-out type.
A possible solution would be enabling ImprediactiveTypes
and having
extract :: (forall s. ST s (Bool, Foo s)) -> (Bool, forall s. ST s (Foo s))
But there's a reason why ImpredicativeTypes
is rarely used: it's poorly integrated with the rest of the type checking machinery and has a tendency to fail for all but the simplest purposes. As an example, assuming extract
with the modified type, the following example doesn't typecheck:
testBar :: IO (Bool, forall s. ST s (Foo s))
testBar = (\(b, sf) -> (b, sf)) <$> testBar'
where
testBar' :: IO (Bool, forall s. ST s (Foo s))
testBar' = return $ extractFoo $ splitChangeFoo newFoo
But it does typecheck when we change (\(b, sf) -> (b, sf))
to (\x -> x)
! So let's forget about impredicative types.
The usual solution is to just wrap the quantified types in a newtype. This is a lot less flexible than impredicative types, but at least it works.
{-# LANGUAGE RankNTypes #-}
import Control.Monad.ST
import Control.Applicative
import Control.Arrow
data Foo s = Foo { foo::Bool }
newtype ST' f = ST' {unST' :: forall s. ST s (f s)}
newFoo :: ST s (Foo s)
newFoo = return $ Foo False
splitChangeFoo :: ST s (Foo s) -> ST s (Bool, Foo s)
splitChangeFoo = fmap (\(Foo b) -> (b, Foo (not b)))
extract :: (forall s. ST s (Bool, Foo s)) -> (Bool, ST' Foo)
extract s = (runST $ fst <$> s, ST' $ snd <$> s)
testBar :: IO (Bool, ST s (Foo s))
testBar = (\(_, ST' s) -> second unST' $ extract $ splitChangeFoo s) <$>
(return $ extract $ splitChangeFoo newFoo)
So, whenever you need to return quantified types, wrap it in a newtype, then unwrap as needed. You might want to stick to a common type argument scheme for you custom types (for example, always make the s
parameter the last, so you can use a single ST'
type for multiple to-be-wrapped types).