Question

I am reading the source of Snap recently, which is great, but when I moved on to read the Snaplet Handler source, I got stuck at the abstraction of RST,Lensed and LensT.

newtype RST r s m a = RST { runRST :: r -> s -> m (a, s) }
newtype LensT b v s m a = LensT (RST (Lens b v) s m a)
newtype Handler b v a = Handler (LensT (Snaplet b) (Snaplet v) (Snaplet b) Snap a)

Now LensT changed to Lensed

newtype Lensed b v m a = Lensed { unlensed :: ALens' b v -> v -> b -> m (a, v, b) }

And the Snaplet Design said We switched to a slightly more specialized monad formulation called Lensed that avoids traversal of the whole state hierarchy when the state is manipulated.

I feel like there is a gap between the implementation of Snap and Snaplet Handler, and the key is RST, LensT and Lensed, is there any reference documentation to help me out?

Was it helpful?

Solution

TL;DR - There's no gap. The Handler definition you pasted is out of date. The current one uses Lensed.

Long answer: We don't have any documentation on this because it is a low-level implementation detail--i.e. all of it is completely hidden from the end user. Carl is right that RST is just RWST minus the W, but let's do some deeper investigation. Using the types that you show above, we'll substitute the definition of RST into the LensT definition. This gives us the following substitutions:

r = Lens b v
s = s
m = m
a = a

With that we can easily write the expanded LensT definition:

newtype LensT b v s m a = LensT { unlensT :: Lens b v -> s -> m (a, s) }

Compare that to Lensed:

newtype Lensed b v m a = Lensed { unlensed :: ALens' b v -> v -> b -> m (a, v, b) }

If we assume that Lens b v and ALens' b v are interchangeable (which, conceptually speaking, they are), then you can see this equivalence:

Lensed b v m a = LensT b v (b,v) m a

Now we see the crux of the issue. LensT is a more general construct than Lensed. LensT allows you to choose your s arbitrarily. Lensed fixes s to be completely determined by b and v. Now that we understand the difference, the question is how are these two constructs actually used in Snap. A quick grep through Types.hs shows us that Handler uses Lensed and Initializer uses LensT. (A side note: the definition you give for Handler is not the one we're currently using.) Here are the important parts of the definitions.

Handler b v a = Handler (L.Lensed (Snaplet b) (Snaplet v) ...)
Initializer b v a = Initializer (LT.LensT (Snaplet b) (Snaplet v) (InitializerState b)...)

Initializer uses the more general LensT construction because it needs s to be InitializerState, which contains extra information not related to b and v. In fact, the whole point of Initializer's existence is to facilitate the construction of a b that will be used as Handler's initial state. Initializer runs when your application starts up, but Handler is what your application runs in. We wanted Handler to be as efficient as possible, so we created Lensed to be optimized for exactly the needs of Handler. You could argue that was premature optimization, but since someone else did it for me, I wasn't going to say no.

You might wonder why we even have Lensed and LensT at all. They're only used in one place so we could just substitute their definitions into Handler and Initializer respectively. That's because we didn't have Lensed at the very beginning. Both Handler and Initializer were written in terms of LensT, and therefore it was a perfectly reasonable abstraction to eliminate duplicated code. Lensed came later and since they are all newtypes anyway, those layers of abstraction impose zero runtime cost.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top