Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal for profunctor implementation #2

Closed
wants to merge 5 commits into from

Conversation

scott-christopher
Copy link
Member

Ok, so I may have gotten a little carried away here 😄

This started out as an investigation into integrating my existing proof of concept on prisms, however I became curious to see how well a profunctor lens library could be implemented in plain ol' JS after looking into the https://github.com/purescript-contrib/purescript-profunctor-lenses project. Turns out that while there are a few moving parts, it came together in a pretty straightforward manner.

n.b. The purpose of this PR is to gauge interest into this style of lens over the existing van Laarhoven style, not necessarily to replace the great work that @DrBoolean has already done here.

WTFunctor is a Profunctor?
A profunctor can be thought of as being similar to regular functor, except it also allows for contravariantly mapping over a second type argument via lmap (or both type arguments at the same time via dimap). Consider the type of a function Function a r, where a is the type of argument it can receive and r is the type of result it will return. A Function a r is a fairly straightforward instance of a Profunctor, where dimap :: (b -> a) -> (r -> s) -> Function a r -> Function b s which you can think of as a way of creating a new function that changes the original function's argument before it receives it and modifying the result before returning it to the caller.

How does it all work?
Good question... In an attempt to summarise my understanding: the various optics (lenses, prisms, folds, isos) are constructed using the profunctor classes (src/Internal/Profunctor/Class/*.js) and are consumed by functions that use the various profunctor implementations (src/Internal/Profunctor/*.js) to interact with the focal point of the optics. e.g. the Forget type performs the same job that Const does in van Laarhoven style lenses for viewing the focal point. Composition of the optics happen in much the same way as van Laarhoven style. If there is some interest in this then I'll write up some docs explaining this in more detail, however I'm happy to attempt to explain further here if people would like to go deeper.

What's with all the monoids?
There is a large selection of monoids in src/Internal/Monoid that are primarily used to implement the various Fold functions. These would benefit being extracted into an external library if this is to be merged.

@DrBoolean
Copy link
Collaborator

OMG! This is amazing. Blown. away.

Let's merge the heck out of it.

Sidenote, I was looking for a good monoid library. I've reimplemented the same ones over and over again. Perhaps that should be part of ramda-fantasy

@DrBoolean
Copy link
Collaborator

@CrossEye @buzzdecafe @davidchambers you down with this magic?

@CrossEye
Copy link
Member

CrossEye commented Feb 3, 2016

WTFunctor is a Profunctor?

I thought it was simply a Functor that had lost its amateur status.

you down with this magic?

It's going to take several more readings for me to understand it.

...

At least.

@DrBoolean
Copy link
Collaborator

Haha right on. If it helps, I have a quick blog post on contravariant functors.
https://medium.com/@drboolean/monoidal-contravariant-functors-are-actually-useful-1032211045c4#.lyalwqrmg

Profunctors are just functors that also have a contramap. In other words dimap is just:

const dimap = (f, g, x) => x.contramap(f).map(g)

//:: String -> TraversalP (Object a) a
const ixObject = k =>
Wander.wander((of, coalg, obj) =>
obj.hasOwnProperty(k) ? R.map(v => R.assoc(k, v, obj), coalg(obj[k]))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use R.has in case obj has an own property named 'hasOwnProperty'. ;)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

592b00f

@davidchambers
Copy link
Member

you down with this magic?

This is all new to me. Scott's patches are always of high quality, so please don't wait for me to learn about profunctors before merging this pull request. :)

I did raise two minor issues.

@scott-christopher
Copy link
Member Author

I'll squash these extra commits down if we decide to go ahead and merge.

@gilligan
Copy link

gilligan commented Feb 3, 2016

This does look pretty great even though I am far from grasping all of it. That being said I for one would for now probably be more interested in turning what is available right now into a documented and tested npm package that I could use in production. This PR would rather delay that I suppose?

The implementation details are beyond me ;) @DrBoolean thanks for that blog article that actually made sense. I just can't fit together the pieces of how pro functors are suitable for implementing lenses yet. Would have to spend some time reading the sources I guess.

@scott-christopher yay for property tests! I wonder why you export the describe functions in the laws modules though? No need unless I am missing something?

@scott-christopher
Copy link
Member Author

@gilligan: That being said I for one would for now probably be more interested in turning what is available right now into a documented and tested npm package that I could use in production.

I suspected that may be the case for some, which is why I'm open to the idea of this living elsewhere and letting the existing lens implementation grow a little.

I wonder why you export the describe functions in the laws modules though?

It was primarily to allow the laws to be reused in multiple tests by passing in different arguments, while attempting to reduce the amount of boilerplate in the test files. There's no reason why the laws' describes couldn't live in the individual test files, but I figured they just be duplicated.

@scott-christopher
Copy link
Member Author

I've also updated this PR to also include a README with attribution to the purescript-profunctor-lens and Kmett's lens libraries, along with a .travis.yml file for running automated tests.

@DrBoolean
Copy link
Collaborator

I'm adding this PR as my pick for jsair this week :)

@CrossEye
Copy link
Member

CrossEye commented Feb 3, 2016

@scott-christopher:

@gilligan: That being said I for one would for now probably be more interested in turning what is available right now into a documented and tested npm package that I could use in production.

I suspected that may be the case for some, which is why I'm open to the idea of this living elsewhere and letting the existing lens implementation grow a little.

OTOH, we have not pulled the previous Lenses out of Ramda core yet. Perhaps this project could serve for now to investigate the profunctor-based one, or maybe investigate both in parallel, but leaving the existing infrastructure in Ramda until this settles down into a useful consensus.

@gilligan
Copy link

gilligan commented Feb 3, 2016

If there is some interest in this then I'll write up some docs explaining this in more detail, however I'm happy to attempt to explain further here if people would like to go deeper.

This is me raising interest. How does all of this work? ;)

@scott-christopher
Copy link
Member Author

I've included a bit of a walk-through of how over and view operate with lenses below. There are obviously other things besides lenses included in this PR, but hopefully this will provide a hint towards how the other optics work too.


Lens construction

// lens :: (s -> a) -> (s -> b -> t) -> Lens s t a b
lens = R.curry((getter, setter) =>
        lens_(s => Tuple(getter(s), b => setter(s, b))));

// lens_ :: (s -> Tuple a (b -> t)) -> Lens s t a b
lens_ = R.curry((to, pab) =>
        PF.dimap(to, t => Tuple.snd(t)(Tuple.fst(t)), Strong.first(pab)));

// An example foo lens that focuses on the 'foo' property of an object
fooL = lens(R.prop('foo'), R.flip(R.assoc('foo')));

// evaluating `lens` into the `lens_` representation
fooL = lens_(s => Tuple(R.prop('foo', s),
                        b => R.assoc('foo', b, s)));

// lens_ produces a function that accepts a profunctor to do different things with the focus
fooL = pab => PF.dimap(s => Tuple(R.prop('foo', s), b => R.assoc('foo', b, s)),
                       t => Tuple.snd(t)(Tuple.fst(t)),
                       Strong.first(pab));

Using over

// over :: Setter s t a b -> (a -> b) -> s -> t
over = R.curry((l, f, s) => l(f)(s));

// uses `over` to create a function that increments the value of the `foo` property
incFoo = over(fooL, R.inc);
// e.g.
incFoo({ foo: 1 }); // 42

// `over` simply applies the given function to the lens, so `R.inc` takes on the role of the profunctor
incFoo = PF.dimap(s => Tuple(R.prop('foo', s), b => R.assoc('foo', b, s)),
                  t => Tuple.snd(t)(Tuple.fst(t)),
                  Strong.first(R.inc));

// Evaluating `first` of `R.inc` results in a function that accepts a Tuple and applies `R.inc` to the first element
incFoo = PF.dimap(s => Tuple(R.prop('foo', s), b => R.assoc('foo', b, s)),
                  t => Tuple.snd(t)(Tuple.fst(t)),
                  t => Tuple(R.inc(Tuple.fst(t)), Tuple.snd(t)));

// Evaluating `dimap` over a function results in a composition of the functions `dimap = (f, g, p) => g(p(f(a))`
incFoo = R.compose(t => Tuple.snd(t)(Tuple.fst(t)),
                   t => Tuple(R.inc(Tuple.fst(t)), Tuple.snd(t)),
                   s => Tuple(R.prop('foo', s), b => R.assoc('foo', b, s)));

// Reducing `compose(f, g, h)` down to `compose(f, gh)`
incFoo = R.compose(t => Tuple.snd(t)(Tuple.fst(t)),
                   s => Tuple(R.inc(R.prop('foo', s)), b => R.assoc('foo', b, s)));

// Finally reduce the `compose` away
incFoo = s => R.assoc('foo', R.inc(R.prop('foo', s)), s);

Using view

// view :: Getter s t a b -> s -> a
view = Fold.foldOf(UnitM);
//                 ^ A monoid type is required to satisfy `Forget`
//                   below but is not used for lens types, so a
//                   simple `UnitM` monoid type is given.

// foldOf :: Monoid m => Type m -> Fold m s t m b -> s -> m
foldOf = R.curry((M, p, s) => p(Forget(M)(x => x)).runForget(s));

// A simple `Getter` that extracts the value of an object's `foo` property
getFoo = view(fooL);

// Expanding `view` and `foldOf`
getFoo = s => fooL(Forget(UnitM)(x => x)).runForget(s);

// Expanding `fooL`, where `Forget(UnitM)(x => x))` is used as the profunctor
getFoo = s => PF.dimap(s => Tuple(R.prop('foo', s), b => R.assoc('foo', b, s)),
                       t => Tuple.snd(t)(Tuple.fst(t)),
                       Strong.first(Forget(UnitM)(x => x))).runForget(s);

// Evaluate `first` of the `Forget` instance
getFoo = s => PF.dimap(s => Tuple(R.prop('foo', s), b => R.assoc('foo', b, s)),
                       t => Tuple.snd(t)(Tuple.fst(t)),
                       Forget(x => Tuple.fst(x))).runForget(s);

// Evaluate `dimap` of the `Forget` instance
getFoo = s => Forget(x => R.prop('foo', x)).runForget(s);

// Finally evaluate `runForget` to reveal the getter
getFoo = s => R.prop('foo', s);

@gilligan
Copy link

gilligan commented Feb 4, 2016

@scott-christopher Thanks for providing the extra info. I have to admit I am still somewhat intimidated because I still have mostly no idea what is going on there.

What I would like to ask is: In what way is this Profunctor based implementation better than the van Laarhoven style ? How does it compare in terms of performance ? What exactly is the advantage ?

Thanks.

@gilligan
Copy link

gilligan commented Feb 4, 2016

Meh, sorry for being so negative but this is my personal equivalent of this ramda issue. I just tried looking at the sources and they seem basically impossible to penetrate.

Not having the faintest idea what is going on I tried to go by the tests. Property based tests are nice to ensure that certain laws are guaranteed but in my opinion they should not replace normal unit tests which do a good job at displaying hey, if you stuff this in then that goes out - i.e good documentation.

require('./src/isos'),
require('./src/traversals')
])
require('./src/Cons'),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I am totally on the wrong track you forgot './src/Getter' ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @gilligan, good catch. I've push up an update to include it.

@scott-christopher
Copy link
Member Author

@gilligan

If we were to introduce prisms I'm pretty sure we would end up at a similar point by having to introduce profunctors to handle those (e.g. refer to my previous PoC: scott-christopher/ramda@ff755e1). This approach just embraces profunctors for everything.

I appreciate there are a number of new concepts introduced here, which like I mentioned above, is why I would be happy maintaining this in a separate project if people are uncomfortable with the complexity. However part of the reasoning for creating the ramda-lens was to allow this to expand into things like isos and prisms without fattening up the core Ramda library. The existing lens functions are currently still available in the core Ramda library.

In terms of understanding, if you have a solid grasp on how the existing van Laarhoven style lenses operate then I can attempt to describe and translate in terms of those. Furthermore, if there are parts in particular that don't make sense, please let me know and I will do my best to explain.

One thing I neglected to mention above was the purpose of Strong's first and second, and Choice's left and right functions. They effectively take a profunctor and return a new instance of that profunctor that now accepts and returns a Tuple for Strong or an Either for Choice. For example, this allows handling the notion of choice when it comes to prisms, where a focus may or may not exist.

In terms of the function implementation for Choice and Strong:

first  :: (a -> b) -> (Tuple a c -> Tuple b c)
second :: (a -> b) -> (Tuple c a -> Tuple c b)
left   :: (a -> b) -> (Either a c -> Either b c)
right  :: (a -> b) -> (Either c a -> Either c b)

Property based tests are nice to ensure that certain laws are guaranteed but in my opinion they should not replace normal unit tests which do a good job at displaying hey, if you stuff this in then that goes out - i.e good documentation.

I have some work in progress towards an examples directory that contains a bunch of example uses which would probably go a long way towards this. Documentation is lacking. It is also my least enjoyable task, which is why I wanted to test the waters here first before spending extra time and effort on documenting something that may not get merged.

@gilligan
Copy link

gilligan commented Feb 4, 2016

The existing lens functions are currently still available in the core Ramda library.

That is something I was also wondering about just now. Let us assume for a moment that this PR is going to be merged - and I am by the way not implying that I am against it, I just want to raise some questios/concerns. What exactly is the plan with Ramda then ? Are lenses going to be removed entirely or is ramda-lens going to be a dependency and Ramda itself only exposes the lenses currently available ?

I have some work in progress towards an examples directory that contains a bunch of example uses which would probably go a long way towards this. Documentation is lacking. It is also my least enjoyable task, which is why I wanted to test the waters here first before spending extra time and effort on documenting something that may not get merged.

For what it's worth I started working on https://github.com/gilligan/ramda-lens/tree/pf-documentation in hope to better grasp what is in there. I will probably not get that far without help. Willing to work on this though. Have you already pushed the examples directory anywhere ? I would find any working code snippets helpful right now :)

@gilligan
Copy link

gilligan commented Feb 4, 2016

By the way..

ramda-lens = 🐏 🔎

@tel
Copy link

tel commented Feb 4, 2016

Some notes from my understanding of Profunctor lenses

  • They're much more natural. In particular, the duality of prisms and lenses is painfully clear in this formulation unlike the van Laarhoven one.
  • Pure profunctor lenses make for awkward traversals. This might just be the price of admission, but in an untyped setting there might be better leverage here, too, actually. It'd be interesting to explore!
  • As noted, Prisms pretty much need the profunctor concept anyway.
  • I'll add: Prisms are amazing, but probably less amazing in Javascript since sum types are so under emphasized. Though, Ramda obviously helps here and tries to ignore that problem.
  • Finally, everyone intersted here should probably read this issue on purescript-lens that @kmett, maintainer of the Haskell lens library, left entitled "Consider 'pure profunctor' lenses" Consider "pure profunctor" lenses purescript-deprecated/purescript-lens#26

@gilligan
Copy link

gilligan commented Feb 5, 2016

@scott-christopher I am playing around with this but got stuck when trying mapped:

const xLens = L.lens(R.prop('x'), R.assoc('x');
L.over(R.compose(L.mapped, xLens), R.inc, [{x:1}]); // [ { x: { x: 1 } } ]

What am I doing wrong here ?

// _2 :: Lens (Tuple c a) (Tuple c b) a b
const _2 = Strong.second;

// atObject :: String -> LensP (Object a) (Maybe a)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is LensP ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LensP s a is shorthand for the type Lens s s a a, indicating the lens doesn't change the type of its subject or focus.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what would the P stand for then ? preserving perhaps ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theses types were following the convention of the purescript-profunctor-lenses types, to which I presume followed the convention of Kmett's type aliases suggesting the P suffix is an abbreviation of Prime because ' isn't a valid character in a PureScript type name.

Perhaps an _ suffix might be more appropriate.

@scott-christopher
Copy link
Member Author

@gilligan: Your lens construction of xLens needs to flip the argument order of the setter to match the type of lens :: (s -> a) -> (s -> b -> t) -> Lens s t a b:

xLens = L.lens(R.prop('x'), R.flip(R.assoc('x')));
example = Setter.over(R.compose(Traversal.traversed, Lens._2, Setter.mapped, xLens), R.inc);
example([ RF.Tuple('foo', RF.Maybe.Just({x: 1})),
          RF.Tuple('foo', RF.Maybe.Nothing()),
          RF.Tuple('foo', RF.Maybe.Just({x: 2})) ]);

// [ Tuple("foo", Maybe.Just({"x": 2})),
//   Tuple("foo", Maybe.Nothing()),
//   Tuple("foo", Maybe.Just({"x": 3})) ]

@gilligan
Copy link

gilligan commented Feb 6, 2016

@scott-christopher Oops, right. I was being ignorant about the fact that the signature for the setter changed ...

L.set(L.lens(R.prop('x'), R.flip(R.assoc('x'))), 1, {x:0})

versus

R.set(R.lens(R.prop('x'), R.assoc('x')),1, {x:0}).

@scott-christopher
Copy link
Member Author

I think we'd probably be better off continuing with the existing argument order of the R.lens, as it means more existing ramda functions could drop right in (such as R.assoc).

I'll push up a change shortly.

@scott-christopher
Copy link
Member Author

@gilligan: I've pushed up 66ec175 to switch the argument order of the setter function given to lens.

@DrBoolean
Copy link
Collaborator

I'm loving this PR convo, but shall we merge?

@CrossEye
Copy link
Member

CrossEye commented Feb 7, 2016

I'm loving this PR convo, but shall we merge?

Ramda's style is to be very quick to merge the ones that cause little debate, and much slower to merge those which cause some more conversation, even when it's mostly positive.

Think well-aged wine. 🍷

@DrBoolean
Copy link
Collaborator

Haha makes sense.

On Feb 6, 2016, at 4:25 PM, Scott Sauyet notifications@github.com wrote:

I'm loving this PR convo, but shall we merge?

Ramda's style is to be very quick to merge the ones that cause little debate, and much slower to merge those which cause some more conversation, even when it's mostly positive.

Think well-aged wine.


Reply to this email directly or view it on GitHub.

@gilligan
Copy link

gilligan commented Feb 7, 2016

Glad to be part of this 🍷 tasting then. I don't think I will be able to provide useful feedback on the implementation - I am still struggling with using its public api ;)

I like the idea of moving lenses out of Ramda and into ramda-lens. At the same time I would like it to be approachable and used by people already using Ramda - moving not removing. I feel that currently we are more in the ramda-fantasy domain.

Perhaps we could thus create some follow-up tickets ?

  • unit tests: like I said before I find the property tests not that great for documenting how they work
  • public api documentation: I started with some documentation but obviously I am not the best candidate for finishing this on my own since I don't quite understand everything
  • examples: @scott-christopher said he has some wip examples. Those would be great

Just my 2 cents.

@gilligan
Copy link

gilligan commented Feb 7, 2016

PS: could someone provide me with (basic) examples for the Foldable/Traversable stuff? I don't quite grok it yet.

@scott-christopher
Copy link
Member Author

@gilligan A summary of the various optics may help a little:

  • A lens will focus on exactly one thing
  • A prism will focus on zero or one things
  • A traversal will focus on zero or many things
  • An iso will transform something between two isomorphic types
  • Prisms, lenses and isos are valid traversals
  • A fold will produce a single result from the zero or many foci of a traversal, using a Monoid's empty to produce the result when zero and concat when many.
  • A getter produces a single result from one focus of an iso or lens
  • A setter can apply a function over zero or many foci of a traversal

This diagram from https://hackage.haskell.org/package/lens shows their hierarchy, such that the composition of any two of them will become the type of their closest common ancestor.
lens relationship diagram
Using the examples from Kmett's lens docs, looking at the diagram you can see:

  • Any Traversal can be used as a Fold or as a Setter.
  • The composition of a Traversal and a Getter yields a Fold.

For some examples of folds, many of the functions in src/Folds.js are actually applications of folds:

sumOf = R.curry((p, s) => foldMapOf(Additive, p, Additive, s).toNum);
// Using the monoid from `Internal/Monoid/Additive.js`
Additive = n => ({
  toNum: n,
  concat: other => Additive(n + other.toNum)
});
Additive.empty = () => Additive(0);

And perhaps the most straightforward way to make sense of traversals would be to just use traversed which is a traversal that supports any valid traversable as its target.

@gilligan
Copy link

gilligan commented Feb 8, 2016

@scott-christopher thanks for that.
So what does everyone else think about the points I have addressed in my last comments and in general: when/if/how do you this should be merged? @CrossEye @DrBoolean @tel @davidchambers ? ;)

Like I said before to me it is not only about how cool this approach with this is or not but how it will play out in relation to Ramda and (re-)moving lens code from there at some point down the road.

Interested to hear everyone's opinion

language: node_js
sudo: false
node_js:
- "stable"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@scott-christopher, you could rebase to remove this file from the diff. :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebased ⚡

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. The ES6 syntax won't play nicely with node v0.12.

@scott-christopher
Copy link
Member Author

I'm beginning to think this implementation may be better off living in its own external project. I'll leave the PR open for another day if anyone wants to comment before closing and making the move.

@gilligan
Copy link

gilligan commented Feb 9, 2016

Now i am starting to feel guilty for derailing this ;)

@CrossEye
Copy link
Member

CrossEye commented Feb 9, 2016

@gilligan: You should not feel the least bit guilty. I think you are one of the only ones trying to follow this closely. @scott-christopher has clearly done some incredible work. But it's not even close to clear whether this makes sense as a replacement for the existing style of lenses we'd been working on.

I have not had any time to look into it, and even when I do, I'm slow to absorb this stuff.

The reason to pull this into its own repo was to be able to play around with it without doing too much potential harm to Ramda itself. It's more of a playground. I'm fascinated to see what Scott is playing with here. But I'm also a bit horrified, the way I am every time I look at the huge API of Haskell Lenses. (The overview page lists 80+ modules, each containing varied number of types, classes, and functions... that's scary!)

So I don't get it, not yet. I understand (after some time) our current Lens implementation, and the reasons @davidchambers would like to remove a number of current functions in favor of it. But I have no clue yet what something like this is for. If it's kept around, I'll eventually take a look and try to learn what it's all about, but I don't have a driving desire to use more advanced Lenses, so it's not top priority for me.

@tel
Copy link

tel commented Feb 9, 2016

I would argue, just for the sake of balance here, that the eldritch horror that truly is Haskell's lens library is... not really a necessary point in design space for a lens library.

Lenses can be rather simple, but they offer a point of view on how one works with data that's quite divergent from what you usually get in most languages. In particular, in Haskell, after going through several lens libraries which failed to address this change of perspective head on the lens library grew which goes completely "batteries included" to ensure that its users can live in a world which exists as if lenses were always known to be the right choice for data access and modification.

That last bit is a huge task and I doubt anything any other library would dare replicate without lots of impetus. Again, for Haskell it was the impetus of about 3 or 4 previous failed lens libraries which drove the creation of lens.


I'd also like to offer something which I've done in private in the past which might be useful to the Ramda team when considering adopting pure profunctor lenses: I'll give a shot at explaining the whole theory in a lightweight way.

Apologies to @scott-christopher who I am sure could do this as well and may indeed have done it already.


First, a perspective on lenses and prisms which gets at their heart and strips away a lot of baggage.

Lenses and prisms both offer a means to relate data types to "subparts". In the same way that products and sums (tuples and case statements) are fundamental to data structure theory they are fundamental to the theory of subparts that lenses embody.

In particular, a value of Lens s a indicates that s is isomorphic to a product of a and "something else"

Lens s a ====> exists x . s ~ Tuple a x

then, dually, a Prism s a indicates that s is isomorphic to a sum of a and "something else"

Prism s a ====> exists x . s  ~ Either a x

If we assume a type of isomorphisms then Lens and Prism could literally be defined as above. It's easy to see how to write standard lens combinators atop values of Tuple a x and Either a x, e.g.

What is a profunctor?

A profunctor is any type with two type parameters which you can think of as being functor-like in one of the two and "inverse functor"-like in the other. To be much more clear, Profunctors need to define dimap

dimap :: (a' -> a) -> (b -> b') -> (p a b -> p a' b')

The canonical example of a profunctor is the regular old function arrow. For a -> b we have that dimap pre-composes its first argument and post-composes its second argument. This is essentially what profunctors do—they let you slap transformations on to their "front" and "tail".

Unlike functions, profunctors don't necessarily "link" the input and the output. For instance, here's a profunctor (in Haskell syntax, sorry)

data Whazzat a b = Whazzat (a -> Bool) b

and there's no particular compulsion for the two parts of Whazzat to "fit together".

But that said, in practice we almost always assume that the do somehow fit together. In that sense, profunctors are generalizations of functions.

How do we transform profunctors?

Pretty much all "pure profunctor" lens types look a bit like this

Profunctor p => p a a -> p s s

In other words, they're transformations of profunctors. The secret of pure profunctor lenses is that by considering a few refinements of profunctor interfaces we can capture all of the data needed to have a lens or prism in "the means to transform a profunctor" and then it ends up looking very pretty and having great composition properties.

The classical example is just what's written above:

 type Iso s a = Profunctor p => p a a -> p s s

The only way to write such a function is to use methods in the Profunctor interface of which there is only 1. Thus, if we have any value i :: Iso s a we know it must be of the form

i = dimap fwd bck
  where
    fwd :: s -> a
    bck :: a -> s

which shows us that we can at least use Iso to smash together all of the pieces we need in order to have an isomorphism (e.g. the forward and backward transformations). We can even extract these two pieces through a tricky choice of profunctor to pass through the transformation

data Extract i o s a = Extract (s -> i) (o -> a)
extract0 = Extract id id :: Extract s a s a

instance Profunctor (Extract i o) where
  dimap f g (Extract si oa) = Extract (si . f) (g . oa)

Getting to Prisms and Lenses

So now we smash all of these pieces together. We'd like to make profunctor transformations which somehow interact with products and sums properly. The way to make this work is to introduce two orthogonal refinements of the profunctor class

class Profunctor p => Strong p where
  first :: p a b -> p (Tuple a x) (Tuple b x)

class Profunctor p => Choice p where
  left :: p a b -> p (Either a x) (Either b x)

Each of these allows us to transform an appropriately enriched profunctor in a new way and both of these new ways are similar in that they allow the profunctor to "ignore" either some product piece or some sum piece. We can use these classes to refine Iso into two new (but familiar) types

type Lens s a = Strong p => p a a -> p s s
type Lens s a = Choice p => p a a -> p s s

And this is where the beauty of pure profunctor lenses comes through. No longer are prisms some weird amalgamation of functionality but are instead a very simple dual structure to lenses just like we thought they might be.

Values of Prism and Lens

Let's assume we've got l :: Lens s a and p :: Prism s a. What do we know about these values? They could be similar to the Iso above and just use dimap as their definition, but since we've invoked the power of Strong and Choice let's use it.

A good "canonical form" for l might be

l :: Lens s a
l = dimap fwd bck . first
  where
    fwd :: s -> Tuple a x
    bck :: Tuple a x -> s

which means we've essentially just popped a call to first in front of an isomorphism between s and (a, x) for some unknown x. This is exactly what we said a lens was above. A good canonical form for p might be

p :: Prism s a
p = dimap fwd bck . left
  where
    fwd :: s -> Either a x
    bck :: Either a x -> s

so the same thing is going on here

And that's the core of it

Pure profunctor lenses are nice because they really expose the heart of what lenses, prisms, and isomorphisms actually are and mean. You could write this core definition down directly, but the "profunctor transformer" type is a clever encoding which captures exactly the information we need while allowing (a) very nice composition and (b) automatic and transparent type hierarchy casting.


Hopefully this helps a bit more than it confuses :)

@scott-christopher
Copy link
Member Author

@gilligan No need to feel guilty. I'm thankful you've shown interest and taken the effort to thoroughly review this PR.

@SimonRichardson
Copy link

@scott-christopher amazing work, i've been work on porting the purescript-profunctors as well. My implementation is almost the same 👍 Although mine slightly differs over when lifting the function up into a type.

Again, very nice!

Also this is why I've been pushing profunctor specification into fantasy-land.

@scott-christopher
Copy link
Member Author

FYI - this has found a new home here: https://github.com/flunc/optics

@SimonRichardson: It'd be great to make use of your profunctor lib as an external dependency. Is there anything publicly available yet? I plan on moving the monoids out into their own library too, so that would effectively remove the need for the current Internal code.

@buzzdecafe
Copy link
Member

@tel thanks for that detailed explanation. I think I will have to reread once or twice to really grasp it. I think you and I may have different definitions of "lightweight" 😃. I am grateful you took the time to shed some light on the subject.

@tel
Copy link

tel commented Feb 10, 2016

@buzzdecafe Haha, "lightweight" here means that I'm leaving out a lot of really nice side benefits to just tell the core story... Unfortunately that doesn't mean the core story is simple!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants