…is useful. And I’ll show you why and how. Multiple persons have told me that this topic is too basic to demand its own post, but I personally was struggling to find a good reference on it. So, without further ado…
Note: I use a word “context” a lot. Sometimes it means a Monad. Refer to my previous article about monads.
Assume you have a data type for state:
and a context:
Someone gives you a nice funky computation you want to run in your context:
You can add an
Int to your context, turning it into:
What do you do?
Well, actually it’s not totally obvious for everyone. I’ll give you a solution right now and then we’ll talk about possible improvements to it. Here’s a function you need:
Make sure you understand the signature, and why we need it to look that way. We have a computation in some
narrow state (
Int) and we want to hoist it to a computation in a broader state (
Now we can use it!
Okay, I promised you an explanation, so here it is. We can’t use the
super function directly, because it expects a context
that’s precisely of type
Int. A record won’t do. A pair of
Ints won’t do. You have to tell it exactly on what part of your state
it’s supposed to operate on. And that’s what
hoistVal is doing; it takes a combinator (
fn) and makes it “think” that it’s actually
operating in a narrower state.
Note that we had to apply
5 to make it compatible; its type is
Int -> State Int Int, but we assume that we’re going to
hoist a “ready-to-use” computation, typed
So, as you can see what
hoist is doing is mimicking another context!. We can do it because we already have required infrastructure in place;
we just have to tell the compiler how to connect it all together.
Technically, yes, because that’s the entire mechanism. I’ve noticed a few additional tricks you can use in your code, though.
super shouldn’t have the signature it has. Don’t write your functions like that! It makes it much harder for people to use it afterwards.
Consider an example, in which you might need
IO to print from your state combinator for whatever bad reason, but the type incidentally matches
the one needed by the function:
Can you run
super in this context? No, because there’s a mismatch between
StateT Int Id a (that’s what
State Int a boils down into if you use transformers) and
StateT Int IO a. But since you don’t care about
Id (you really only want any
State that has
Int inside), it should use
It’s a really useful little thing that resides in
What it does is basically defining an interface that every stateful context can implement.
Note: you need
FlexibleContexts for that, despite the fact that if you don’t enable it and omit the signature, GHC will infer it correctly.
This means this function will work for every context
Int. Now we could use it in both our hoisted state, raw
State Int, or that
If you look closer, you’ll realize that
hoist actually has the same problem!
Cool, it can now hoist inside of both of regular and transformed variants.
Why not make the first
State another type parameter? After all, we might expect that someone might give us an
StateT Int IO ... action, and then…
Wrong. Haskell doesn’t allow you to mix pure and impure code for a reason, and for the exact same reason that there’s no possibility of
IO a -> a ever working
unsafePerformIO out of that; For all I care, it might not exist), there’s no way to get
StateT s IO a -> State s a to ever compile.
That being said, you can hoist
StateT IO sa x into
StateT IO sb x (or whatever instead of IO);
the only caveat is that you have to replace one line:
So am I. I hope you’ve heard about
Lens. Before I introduce it, let’s see what would happen if we tried to parametrize over
hoist would need to look more or less like:
But sans the fact Haskell doesn’t allow us to use
put (that’s why there’s no
puts, which is kind of unfortunate),
acc’s signature itself makes
it “read-only”. What we need is a way to extract the part of the state and then put it back together.
So a getter and setter pair.
That’s a Lens.
In our case, even
Simple Lens will do:
And now our desired syntax works perfectly. We can freely nest records, and the lenses will take care of wrapping and unwrapping.