-
Notifications
You must be signed in to change notification settings - Fork 15
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
Conversation
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 |
@CrossEye @buzzdecafe @davidchambers you down with this magic? |
I thought it was simply a Functor that had lost its amateur status.
It's going to take several more readings for me to understand it. ... At least. |
Haha right on. If it helps, I have a quick blog post on contravariant functors. Profunctors are just functors that also have a 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])) |
There was a problem hiding this comment.
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'
. ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⚡ 592b00f
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. |
I'll squash these extra commits down if we decide to go ahead and merge. |
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? |
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.
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' |
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 |
e6d4205
to
dd189c7
Compare
I'm adding this PR as my pick for jsair this week :) |
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. |
This is me raising interest. How does all of this work? ;) |
I've included a bit of a walk-through of how 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 :: 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 :: 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); |
@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. |
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 |
require('./src/isos'), | ||
require('./src/traversals') | ||
]) | ||
require('./src/Cons'), |
There was a problem hiding this comment.
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'
?
There was a problem hiding this comment.
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.
dd189c7
to
e1ebde8
Compare
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 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 In terms of the function implementation for
I have some work in progress towards an |
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 ?
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 :) |
By the way..
|
Some notes from my understanding of Profunctor lenses
|
@scott-christopher I am playing around with this but got stuck when trying 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is LensP
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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.
@gilligan: Your lens construction of 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})) ] |
@scott-christopher Oops, right. I was being ignorant about the fact that the signature for the setter changed ...
versus
|
I think we'd probably be better off continuing with the existing argument order of the I'll push up a change shortly. |
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. 🍷 |
Haha makes sense.
|
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 ?
Just my 2 cents. |
PS: could someone provide me with (basic) examples for the Foldable/Traversable stuff? I don't quite grok it yet. |
@gilligan A summary of the various optics may help a little:
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.
For some examples of folds, many of the functions in 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 |
@scott-christopher thanks for that. 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" |
There was a problem hiding this comment.
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. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rebased ⚡
There was a problem hiding this comment.
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.
66ec175
to
9c6c8e1
Compare
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. |
Now i am starting to feel guilty for derailing this ;) |
@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. |
I would argue, just for the sake of balance here, that the eldritch horror that truly is Haskell's 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 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 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
then, dually, a
If we assume a type of isomorphisms then 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
The canonical example of a profunctor is the regular old function arrow. For Unlike functions, profunctors don't necessarily "link" the input and the output. For instance, here's a profunctor (in Haskell syntax, sorry)
and there's no particular compulsion for the two parts of 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
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:
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
which shows us that we can at least use
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
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
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 Let's assume we've got A good "canonical form" for
which means we've essentially just popped a call to
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 :) |
@gilligan No need to feel guilty. I'm thankful you've shown interest and taken the effort to thoroughly review this PR. |
@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. |
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 |
@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. |
@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! |
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 viadimap
). Consider the type of a functionFunction a r
, wherea
is the type of argument it can receive andr
is the type of result it will return. AFunction a r
is a fairly straightforward instance of a Profunctor, wheredimap :: (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. theForget
type performs the same job thatConst
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 variousFold
functions. These would benefit being extracted into an external library if this is to be merged.