Question

In the Snap Framework, Snaplets are used to embed functionality into other Snaplets via a component-based interface: The main web application is a Snaplet that references other Snaplets via a classical "has-a" relationship, and sub-Snaplets can in turn reference other Snaplets.

When looking at various Snaplet implementations, I've seen different patterns being used to embed a Snaplet into a parent Snaplet. Specifically:

  • Kind of reference. The Snaplet implementation assumes that a specific kind of relation to the parent Snaplet is present. This is enforced via the Reference method used (see below).

    1. A plain reference:

      data MySnaplet = MySnaplet { subSnaplet :: Snaplet SubSnaplet }
      
    2. A relative lens:

      data MySnaplet = MySnaplet { _subSnaplet :: Snaplet SubSnaplet }
      
      subSnaplet :: Lens MySnaplet SubSnaplet
      subSnaplet = lens _subSnaplet $ \ a b -> a { _subSnaplet = b }
      
  • Reference method. The Snaplet implementation enforces, via its interface, that a specific way of accessing the Snaplet data is in place, and different Snaplet implementations use different methods. The Snaplet assumes that:

    1. The data is present in a MonadState every time a function that manipulates the Snaplet is called.
    2. The data is present in a MonadState and wrapped in a Snaplet wrapper.
    3. There is a class+instance like instance HasSubSnaplet MySnaplet that has a function for getting the Snaplet data out of MySnaplet provided that a MySnaplet is in a MonadState at the point of calling the function.
    4. The function in 3. has type MySnaplet -> Snaplet SubSnaplet instead.
    5. There is a class+instance like in 3. that provides a Lens MySnaplet (Snaplet SubSnaplet).
    6. The class+instance requires a Lens (Snaplet MySnaplet) (Snaplet SubSnaplet).
    7. The class+instance assumes that MySnaplet is the "top Snaplet" of the application, and requires an absolute lens/reference, such that MySnaplet must be the b in a MonadSnaplet.

As I see it, reference kind 1. makes sense if the Snaplet is read-only, and 2. makes sense if the Snaplet needs to change.

Further, having a class for the method makes sense when MySnaplet can have only one SubSnaplet and no more, and having an absolute reference might make sense for things like databases, that cannot possibly be configured as a component, given that only the top Snaplet has access to credentials and what not. However, making this assumption as a Snaplet writer might be fallacious, and there would be no disadvantages to using a relative references instead.

There's one proglem, though: Existing Snaplets on Hackage do not fit with these assumptions that I make; all of the methods described above are used seemingly at random and in all kinds of circumstances. Also, I see no advantage/disadvantage to some of the other aspects described above (Such as requiring a Snaplet wrapper, or not).

To me, reference kind 2. and one of methods 1, 2, 5 or 6 seem to make the most sense under all circumstances, and I see no reason why there's not a consensus on only using e.g. (2, 1) all the time.

So:

As a Snaplet writer, which method should be preferred when writing a new Snaplet (assuming that it has a general purpose), and

What is the reason why all Snaplets in existence don't already use the same reference method (Even in the core snap package, a ton of different methods are used)?

Was it helpful?

Solution

TLDR; Most of the time you probably want to use the with function and relative lenses.

The use of a HasSubSnaplet type class is a completely optional pattern that can reduce the "with subSnaplet" boilerplate in situations where it doesn't make sense to have more than one instance of SubSnaplet. We chose to do that for the Heist snaplet that ships in the snap package because it makes sense for the Heist snaplet and provides users an example of the pattern.

Since the type class is completely optional and roughly orthogonal to the choice of lens, for the rest of this answer I'll focus on what to do without the type class.

The intent of the API is that you use lenses (not "plain references" that get you something wrapped in the Snaplet data type) to access your state. This is because mutating state during the course of request processing is a fundamental ability that we wanted Snaplets to provide. For the most part, we intended for Snaplet to be an opaque wrapper that the end user doesn't need to touch except in their snaplet's state type. This is why the MonadState instance gets you directly to your type without the Snaplet wrapper.

With that said, there are four basic access patterns. We can see these looking at the MonadSnaplet type class.

with     :: Lens     v       (Snaplet v') -> m b v' a -> m b v a
withTop  :: Lens     b       (Snaplet v') -> m b v' a -> m b v a
with'    :: Lens (Snaplet v) (Snaplet v') -> m b v' a -> m b v a
withTop' :: Lens (Snaplet b) (Snaplet v') -> m b v' a -> m b v a

The lens pattern embodied in the first two functions is the kind of lenses that are generated for you automatically by the data-lens-template package with TemplateHaskell and are the most natural to use. Therefore, this is the recommended pattern. (Hence the shorter, non-primed name.) The difference between with and withTop is that with takes a relative lens and withTop takes an absolute lens. Most of the time I use relative lenses. But we wanted to allow the use of absolute lenses because I can imagine complex applications where one snaplet might need to get at something provided by another snaplet, but one that is not a descendant of the current snaplet.

Occasionally there are situations where you want to be able to have an identity lens. That requires a Lens (Snaplet a) (Snaplet b). So the second two primed functions are analogous to the first two except they take this kind of lens.

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