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

Profunctor specification #124

Closed
wants to merge 4 commits into from
Closed

Profunctor specification #124

wants to merge 4 commits into from

Conversation

SimonRichardson
Copy link
Member

No description provided.


const composition = t => eq => x => {
const a = p[dimap](compose(identity)(identity), compose(identity)(identity));
const b = p[dimap](identity, identity)[dimap](identity, identity);
Copy link
Contributor

Choose a reason for hiding this comment

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

Should these lines use t(x)[dimap] rather than p[dimap] here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, yes they should.

On Fri, 29 Jan 2016, 22:22 Scott Christopher notifications@github.com
wrote:

In laws/profunctor.js
#124 (comment)
:

+### Profunctor
+
+1. p.dimap(f, g) is equivalent to p (identity)
+2. p.dimap(compose(f1)(f2)), compose(g1)(g2)) is equivalent to p.dimap(f1, g1).dimap(f2, g2) (composition)
+
+**/
+
+const identityʹ = t => eq => x => {

  • const a = t(x)[dimap](identity, identity);
  • const b = t(x);
  • return eq(a, b);
    +};

+const composition = t => eq => x => {

  • const a = p[dimap](compose%28identity%29%28identity%29, compose%28identity%29%28identity%29);
  • const b = p[dimap](identity, identity)[dimap](identity, identity);

Should these lines use t(x)[dimap] rather than p[dimap] here?


Reply to this email directly or view it on GitHub
https://github.com/fantasyland/fantasy-land/pull/124/files#r51324452.

@scott-christopher
Copy link
Contributor

This is great (and quite timely too, as I've recently been writing a JS implementation of the purescript-profunctor-lenses library).

Is it worth including some information about deriving lmap and rmap?

@SimonRichardson
Copy link
Member Author

I was wondering that myself, although it's normally sufficient to say if
you implement dimap, lmap and rmap fallout of the spec naturally.
Maybe a note would be sufficient. I'll add when I get a chance.

On Fri, 29 Jan 2016, 22:36 Scott Christopher notifications@github.com
wrote:

This is great (and quite timely too, as I've recently been writing a JS
implementation of the purescript-profunctor-lenses library).

Is it worth including some information about deriving lmap and rmap?


Reply to this email directly or view it on GitHub
#124 (comment)
.

@joneshf
Copy link
Member

joneshf commented Jan 30, 2016

What about going the other way?, derive dimap from lmap and rmap.

@scott-christopher
Copy link
Contributor

What about going the other way?, derive dimap from lmap and rmap.

Or could/should Profunctor be defined by implementing lmap with a dependency on also implementing Functor?

Adding bifunctor as well because they're very similar. In fact the laws
are identitcal!

 - Also fixing the issue that the laws didn't work
@SimonRichardson
Copy link
Member Author

I've also included bifunctor as well.

@joneshf The issue I've got with lmap and rmap is that it makes the spec more complicated and I'm unsure of how to word it nicely. If someone wants to take a stab at it, be my guest. The issue I don't want to do is cause the of/empty confusion we have now.

Consider what Haskell says:

You can define a Profunctor by either defining dimap or by defining both lmap and rmap.

I just find it a bit confusing, considering that the rest of the spec is just a case (minus of/empty of course 😐 ) of; "implement a function like this, you get the result", but with this implementation you can implement it two ways.

Alternatively maybe I can make a note that lmap and rmap can be derived like the following:

x.prototype.lmap = function(f) {
    return this.dimap(f, identity);
};
x.prototype.rmap = function(g) {
    return this.dimap(identity, g);
};

@SimonRichardson
Copy link
Member Author

@scott-christopher that's an idea, could solve ambiguity?

So dimap could be derived by the following (see below) and there wouldn't be any need to create a rmap at all. Just lmap and dimap, which might also solve @joneshf issue as well.

x.prototype.dimap = function(f, g) {
     return lmap(f).map(g);
}

(sorry i'm just thinking out aloud here).

@SimonRichardson
Copy link
Member Author

@joneshf I've updated to the specification, what do you think about this one?

Included specifications for `lmap`
@joneshf
Copy link
Member

joneshf commented Feb 26, 2016

After thinking about it for a while. I think the original version is more inline with the rest of the spec. Take Traversable for example. We don't say you can define traverse instead of sequence. Nor do we show how to derive sequence from traverse. Similarly for ap/lift2, chain/join, extend/duplicate, etc. And I think that's probably for the best. As much as I would like to be able to define any random assortment of functions, it's probably best if we don't grow the possible ways to implement the spec too much.

What I think we should do, is stick with dimap and show how you can derive map and lmap. I also think we should do something similar for bimap, map and first?

These names bring up a good point. It's going to be pretty hard to explain the difference between lmap, first and map, plus bimap and dimap. I feel like lmap and first are badly named in isolation, but make sense with rmap and second, respectively. However, rmap ≈ second ≈ map. So that's no good either.

Also, bimap and dimap sound too similar. I have no qualms with the names in haskell or such because there's a type system, but I don't think people with only experience in js will appreciate the names.

functor on its first type parameter and functor on its second type parameter.

1. `p.dimap(f, g)` is equivalent to `p` (identity)
2. `p.dimap(compose(f1)(f2)), compose(g1)(g2))` is equivalent to `p.dimap(f1, g1).dimap(f2, g2)` (composition)
Copy link
Contributor

Choose a reason for hiding this comment

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

Too many bracket?

p.dimap(compose(f1)(f2)),
                       ^

(same thing at bimap)

Copy link
Member

Choose a reason for hiding this comment

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

Also in the rest of the spec function composition is just 'hardcoded', instead of using a helper compose function.

- compose(f1)(f2)
+ x => f1(f2(x))

Copy link
Contributor

Choose a reason for hiding this comment

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

Looking closer I see a whole bunch of mistakes and inconsistencies in the readme changes. I guess we'll get to them once the spec is decided upon. Sorry for the clutter. :)

@Avaq
Copy link
Contributor

Avaq commented Mar 16, 2016

people with only experience in js -- @joneshf

Here's the two cents from the perspective of one such person; me.


Or could/should Profunctor be defined by implementing lmap with a dependency on also implementing Functor? -- @scott-christopher

This made it immediately clear to me what Profunctor is all about. I like this idea. Plus that it adds a lot of clarity to the lmap name. Just like we have reduce and reduceRight, we have map and lmap.


[Confusingly,] with this implementation you can implement it two ways -- @SimonRichardson

I wouldn't like writing abstractions over Profunctor if I'd have to check every time whether it's a Profunctor with .dimap(f, g), or one with .lmap(f).map(g). I think it's good to have a single way to do things.


Maybe it clarifies things if there would be a separate type for lmap, just like was done with Chain / Monad? Implement Lmappable by Functor + lmap(); Derive Profunctor from Lmappable, or implement it by dimap(). Now we can be sure a Profunctor always has dimap(), and we have a different name for the Profunctor version that only has lmap/map.

@Avaq
Copy link
Contributor

Avaq commented Mar 16, 2016

a different name for the Profunctor version that only has lmap/map

I'm not much of an algebraic type, myself, but something like Difunctor sounds like a sensical name to me. It sounds cooler than Lmappable, which I think is really important to mathematicians.

@joneshf
Copy link
Member

joneshf commented Mar 16, 2016

After thinking about it for a while. I think the original version is more inline with the rest of the spec. Take Traversable for example. We don't say you can define traverse instead of sequence. Nor do we show how to derive sequence from traverse. Similarly for ap/lift2, chain/join, extend/duplicate, etc. And I think that's probably for the best. As much as I would like to be able to define any random assortment of functions, it's probably best if we don't grow the possible ways to implement the spec too much.

What I think we should do, is stick with dimap and show how you can derive map and lmap. I also think we should do something similar for bimap, map and first?

@scott-christopher, @SimonRichardson:

After reading the whole issue again, this quoted seciton stands out as though I hadn't read the replies above it. That was some very poor wording. I meant to agree with what you said, and provide an additional example. Sorry if I came off as ignoring what you'd written above

@Avaq
Copy link
Contributor

Avaq commented Mar 20, 2016

How about:

  • Profunctor depends on Functor and implements lmap
  • Difunctor has no dependencies and implements dimap, which can be derived as lmap(f).map(g)
  • Bifunctor has no dependencies and implements bimap

I think that makes a pretty clear spec in which the naming seems logical. I'm throwing in ideas because I'd like to implement this in Fluture according to the spec from the get-go. :)


I don't understand the differences between dimap and bimap, because I don't fully understand covariance and contravariance. What I've taken away from reading this spec in its current form, is that since bimap cannot be derived from lmap and map, it's probably intended for types where the two inners cannot be mapped individually from one another. I would benefit from some clarity surrounding the difference, but I think the names are fine despite being so similar.

@SimonRichardson
Copy link
Member Author

I'm happy with that, but don't have any time to make the changes ATM (life
has just come up, sorry). If somebody wants to make the changes then I
don't mind reviewing.

On Sun, 20 Mar 2016, 18:48 Aldwin Vlasblom, notifications@github.com
wrote:

How about:

  • Profunctor depends on Functor and implements lmap
  • Difunctor has no dependencies and implements dimap, which can be
    derived as lmap(f).map(g)
  • Bifunctor has no dependencies and implements bimap

I think that's a pretty clear spec where the naming makes sense. I'm
throwing in ideas because I'd like to implement this is Fluture
https://github.com/Avaq/Fluture according to the spec from the get-go.

:)

I don't understand the differences between dimap and bimap, because I
don't fully understand covariance and contravariance. What I've taken away
from reading this spec in its current form, is that since bimap cannot be
derived from lmap and map, it's probably intended for types where the two
inners cannot be mapped individually from one another. I would benefit from
some clarity surrounding the difference, but I think the names are fine
despite being so similar.


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub
#124 (comment)

@Avaq
Copy link
Contributor

Avaq commented Apr 23, 2016

Can somebody explain to me what the difference between dimap and bimap is? Here's some of my confusions:

  • I think one could implement lmap into Future, so naturally, you could derive dimap. Does that exclude Future from being a Bifunctor?
  • Surprisingly, Sorella appears to have named it bimap in her implementation.
  • How does the covariance and contravariance mentioned in the spec translate to JavaScript?

@scott-christopher
Copy link
Contributor

scott-christopher commented Apr 24, 2016

@Avaq: It's probably simplest to explain with the signatures to show how the type parameters are swapped:

bimap :: (a -> b) -> (c -> d) -> Bifunctor  a c -> Bifunctor  b d
dimap :: (a -> b) -> (c -> d) -> Profunctor b c -> Profunctor a d

We can implement bimap for Future by applying the first function over the failed result and the second when it is successful:

bimap :: (a -> b) -> (c - > d) -> Future a c -> Future b d

Try to think about how you would take a function a -> b given to lmap :: (a -> b) -> Profunctor b c -> Profunctor a c to convert a Future b c to a Future a c. Unfortunately it's not possible (unless a and b are actually the same type).

Now consider something that represents a function, Fn a b, where a is the type of its expected argument and b is its return type. We can implement dimap by composing the two given functions on either end of the function represented by the Fn type.

class Fn {
  //:: (b -> c) -> Fn b c
  constructor(f) { this.f = f }
  //:: (a -> b) -> (c -> d) -> Fn a d
  dimap(a2bFn, c2dFn) {
    return new Fn(compose(c2dFn, compose(this.f, a2bFn)))
  }
}

But now there is no way to represent bimap, because we can't use the first argument a -> b to change the a in Fn a c to an Fn b c.

I'm not 100% sure about the exact meaning of covariance / contravariance in this context, though I believe it relates to contravariant functors rather than specifically to the notion of sub-typing as in other languages. My intuition around this is just that a contravariant type parameter is used to represent a function argument stating something the type expects to receive, while a covariant type parameter represents something the type can produce.

For example, you might have a Predicate a type where the a is used to indicate the type it expects to receive when run to return a true or false value. Because this type parameter is used to indicate something that Predicate expects to receive, the parameter is considered contravariant. This means that it is possible to use lmap with it, though the regular covariant map cannot.

class Predicate {
  // (a -> Boolean) -> Predicate a
  constructor(p) { this.run = p }
  // (b -> a) -> Predicate b
  lmap(f) {
    return new Predicate(compose(this.run, f))
  }
}

@Avaq
Copy link
Contributor

Avaq commented Apr 24, 2016

It's probably simplest to explain with the signatures -- @scott-christopher

Seeing the signatures did indeed clear things up. The rest of your post helped me understand how the seemingly strange dimap signature comes into play. Thanks a bunch for taking the time to lay it out for me. This made it a whole lot clearer. And the Predicate example is perfect as well since I was just wrapping my head around creating a Predicate type yesterday!

I've misinterpreted lmap it seems. I thought it was simply for mapping over the left value of any kind of functor with two values:

const Future = fork => ({ 
  fork,
   map: f => Future((l, r) => fork(l, compose(r, f))),
  lmap: f => Future((l, r) => fork(compose(l, f), r)) //<- misinterpreted
})

If the above were the case, then deriving dimap by .lmap(f).map(g) would give you a function equal to the bimap you've described. This was the primary source of my confusion.


The fact that I couldn't make out this information from reading the draft spec might indicate that the spec is "incomplete" (or at the very least unclear), or it might just be indicative of my incompetence. ;)

@scott-christopher
Copy link
Contributor

I think I've come to the conclusion that we should just use bimap to define bifunctor and potentially rename dimap to promap for defining profunctor to reduce further chance of the bi vs di confusion as indicated by @joneshf.

I'm also of the opinion now that we should perhaps not mention lmap and first at all (or at most, show how they're derivable). Libraries would still be free to implement them based on promap and bimap, but I think it would make it simpler to have a single way to check whether something implements the spec.

My earlier point about a profunctor simply being a functor with lmap defined is also kinda flawed since it needs to be stated that they're operating over different type arguments.

If we can reach an agreement on the spec, I'd be happy to make any amendments in a separate PR if @SimonRichardson doesn't have the time available.

@SimonRichardson
Copy link
Member Author

@scott-christopher Please do, I'm super busy with work atm, so I trust you to do the right thing :) I'll happily look over for a code review though.

@scott-christopher
Copy link
Contributor

scott-christopher commented May 7, 2016

I have raised #137 with the tweaks suggested above.

@SimonRichardson
Copy link
Member Author

Closing this, as it was superseded with #137

@davidchambers davidchambers deleted the profunctor branch December 27, 2016 00:50
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

5 participants