2

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.

Sridhar Ratnakumar
  • 81,433
  • 63
  • 146
  • 187
  • 1
    A type like `type CtxPrism ctx s a = Prism (ctx, s) s (Maybe a) (ctx, a)` would naively capture it. A slightly better approach (how?) is to somehow compose a `Reader` and a `Prism'`. – Sridhar Ratnakumar Mar 15 '22 at 00:31
  • 3
    I'm not getting what exactly you want beyond literally just `ctx -> Prism' s a`. – HTNW Mar 15 '22 at 10:12
  • @HTNW `ctx` is time-varying and is not a constant value that is known at the time of creation of route encoders. Furthermore, `ctx` is *required* for encoding and decoding correctly, and therefore must be part of the prism function arguments. Think of `ctx` as data from some in-memory database. I've also edited the question with more information. – Sridhar Ratnakumar Mar 15 '22 at 12:27
  • @HTNW Here's another way to think about it: how can the getter functions inside of your `Prism' s a` access the `ctx` outside of it? Secondly, how would you define `encodeRoute` and `decodeRoute` (see edit to question) using your type? – Sridhar Ratnakumar Mar 15 '22 at 12:34
  • 1
    What kinds of context transformations are you doing? Are you *extracting* smaller contexts from larger ones, or are you making more general changes? If you're only doing extraction, you might consider a `reflection`-based approach. – dfeuer Mar 15 '22 at 17:50
  • @dfeuer You can see `leftRouteEncoder` in https://github.com/srid/ema/pull/81 which transforms the context `(a, b)` into `a` (while, at the same time, transforming the `Either r1 r2` route to `r1`). So yea, the transformation can be to extract smaller contexts from larger ones. See also `innerRouteEncoder` which does it generically for any product type. I'll take a look at `reflection` - but TBH I'm not sure if I'll really have to use type-classes here ... – Sridhar Ratnakumar Mar 15 '22 at 21:48
  • More generally, I'm extracting the "inner" route encoder from the "outer" route encoder. And this necessitates drilling down into the "inner" route as well as the *associated* "inner" context. – Sridhar Ratnakumar Mar 15 '22 at 21:50
  • 1
    @SridharRatnakumar, `reflection` is a fun package that gives the class system a bit of a different flavor. It's not so much about "having to" use type classes as about "getting to" use type classes. – dfeuer Mar 16 '22 at 04:25

2 Answers2

3

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
K. A. Buhr
  • 45,621
  • 3
  • 45
  • 71
  • Ah, I had underestimated the nature of composing functions. Intuitively I thought this would be a wrong approach, but you and @HTNW are right. `ctx -> Prism' s route` does work! I'm using this: https://github.com/srid/ema/pull/81/commits/d350e979591fe2ff7af50dc01ab403a28169feeb - though (as the commit shows) I can't yet use `Prism'` due to coercion issues with DerivingVia, but I'll post a new question for that. – Sridhar Ratnakumar Mar 15 '22 at 21:42
  • Posted the new question [here](https://stackoverflow.com/questions/71489589/unable-to-coerce-during-derivingvia-when-using-optics-types-like-prism). – Sridhar Ratnakumar Mar 15 '22 at 22:13
1

If your context is a sort of "configuration" that applies to everything going on in the same region of your program, then you might consider using reflection. You can use

type RouteEncoder ctx s r = ctx => Prism' s r

The context becomes implicit. You'll have something like

ctx ~ Zippity x
s ~ Whatever x

This is only really ergonomical if you have one context that works throughout. However, you can use the native class relationships to add some flexibility there, allowing you to use optics requiring "pieces" of a larger context.

Example

Suppose each configuration aspect is represented as a class:

class FooC x where
  getFoo :: Foo

class BarC x where
  getBar :: Bar

class (FooC x, BarC x) => ConfigC x where
  getBaz :: Baz

Now you'll have various prisms that look roughly like

whatever :: forall x. FooC x => Prism' (Thing x) (Thingum x)
yeah :: forall x. BarC x => Prism' (Thingum x) (Yak x)
uhHuh :: forall x. ConfigC x => Prism' (Yak x) Hum

(Sorry, not the best examples.)

You'll note that none of the prisms need to know where their configuration info comes from; they just need what they need.

When you compose these, you'll get prisms with contexts accumulating the necessary contexts. To actually use these prisms, you'll need one or more helper types. The obvious one here is

data Config = Config
  { _fooConfig :: Foo
  , _barConfig :: Bar
  , _bazConfig :: Baz }

data UsingConfig x = UsingConfig

instance Reifies x Config => FooC (UsingConfig x) where
  getFoo = _fooConfig $ reflect (Proxy @x)
instance Reifies x Config => BarC (UsingConfig x) where
  getBar = _barConfig $ reflect (Proxy @x)
instance Reifies x Config => ConfigC (UsingConfig x) where
  getBaz = _bazConfig $ reflect (Proxy @x)

To actually apply the prisms, you'll need to use reify to toss a Config into the air for them to catch.

If some other program or program component is using a different combination of prisms with different needs, it can use its own master configuration type (like Config) and its own tag type (like UsingConfig) to make the necessary context available.

Sridhar Ratnakumar
  • 81,433
  • 63
  • 146
  • 187
dfeuer
  • 48,079
  • 5
  • 63
  • 167