Search code examples
haskelllenses

What is a `Prism' s a` but with a context `ctx`?


A type Prism' s a = Prism s s a a (hackage) can be thought of as a relation between some structure s and its member a, such that you can always produce the structure from the member (a -> s), but can only optionally retrieve the member from the structure (s -> Maybe a).

This model is helpful in relating a sum type to one of its constructors ... as well as (more relevant here) in route encoding and decoding. If s is the encoded route URL, and a is the route type, then we have a -> s representing the encoding function (always succeeds), and s -> Maybe a representing the decoding function (can fail).

Now, on to these pairs of functions, I want to add a "context" argument that is to be used in the encoding and decoding process (imagine that the decoding process needs to "look up" some database before successfully producing a relevant route). Basically:

encode :: ctx -> a -> s 
decode :: ctx -> s -> Maybe a

Is there a type that models these conversions? It looks very much like a Prism' but with an extra ctx argument.


As a next step, I'd like to define a functor for this prism such that it can transform all three types: ctx, s, a. I currently have a class like this, but it appears I might be missing an existing library that I could use to simplify all of this:

class PartialIsoFunctor (f :: Type -> Type -> Type -> Type) where
  -- x, y are the context
  -- a, b are the structure `s`
  -- c, d are the route types `a`
  pimap ::
    Prism' b a -> -- Note: contravariant prism
    Prism' c d ->
    (y -> x) -> -- Note: this is contravariant
    f x a c ->
    f y b d

The idea here is that there is a RouteEncoder ctx r type (see also) representing a value that knows how to encode/decode routes. And I want to be able to transform these route encoders on the r, ctx, and the URL string (internally a FilePath, actually) it encodes to/ decodes from.

Notes:

  • I use optics-core rather than lens.

EDIT: Here's my current approach:

type RouteEncoder ctx s route = Prism' (ctx, s) (ctx, route)

And the functor that transforms it:

mapRouteEncoder ::
  Prism' b a ->
  Prism' c d ->
  (y -> x) ->
  RouteEncoder x a c ->
  RouteEncoder y b d
mapRouteEncoder = undefined

The encoding/decoding functions:

-- The use of `snd` here suggests that the use of tuples in 
-- RouteEncoder is a bit of a hack

encodeRoute :: RouteEncoder ctx r -> ctx -> r -> FilePath
encodeRoute enc ctx r = snd $ review enc (ctx, r)

decodeRoute :: RouteEncoder ctx r -> ctx -> FilePath -> Maybe r
decodeRoute enc m s = snd <$> preview enc (ctx, s)

How can this be simplified? Note that RouteEncoder's are created ahead, and composed. But the actual encoding/decoding happens later, passing the 'ctx' that varies over time.


Solution

  • I'm with @HTNW. You should just be able to define:

    type RouteEncoder ctx s route = ctx -> Prism' s route
    

    Then, pimap is defined as:

    pimap :: Prism' b a -> Prism' c d -> (y -> x)
      -> RouteEncoder x a c -> RouteEncoder y b d
    pimap p q f r ctx = p . r (f ctx) . q
    

    and encodeRoute and decodeRoute are defined as:

    encodeRoute :: RouteEncoder ctx s r -> ctx -> r -> s
    encodeRoute enc ctx r = review (enc ctx) r
    
    decodeRoute :: RouteEncoder ctx s r -> ctx -> s -> Maybe r
    decodeRoute enc ctx s = preview (enc ctx) s
    

    The only difficulty is that composition of route encoders with deferred context requires some additional syntax. You may need to write:

    \ctx -> otherLens . myRouteEncoder ctx . otherPrism
    

    or similar instead of simple composition. However, your existing solution doesn't compose particularly well with other optics either, unless they are "aware" of the context.

    I'm not 100% sure what you mean when you ask how getter functions inside the prism access the context outside the prism. If you mean at definition time, then the answer is that you just use something like:

    makeRouteEncoder a b c ctx = prism (f a b ctx) (g c ctx)
    
    myRouteEncoder = makeRouteEncoder myA myB myC