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

derivable methods are contradictory #132

Closed
davidchambers opened this issue Apr 11, 2016 · 30 comments
Closed

derivable methods are contradictory #132

davidchambers opened this issue Apr 11, 2016 · 30 comments

Comments

@davidchambers
Copy link
Member

Applicative:

A value that implements the Applicative specification must also implement the Apply specification.

Apply:

A value that implements the Apply specification must also implement the Functor specification.

Functor:

A value which has a Functor must provide a map method.

So a value that implements the Applicative specification must provide a map method.

Here is the contradiction:

A value which satisfies the specification of an Applicative does not need to implement:

  • Functor's map; derivable as function(f) { return this.of(f).ap(this); }

Is there a semantic difference between "implements the Applicative specification" and "satisfies the specification of an Applicative"?

I'm wondering whether Ramda's map function should operate on a value which provides of and ap but not map. The fact that derivations are mentioned in the spec suggests they are desirable, but as written it doesn't seem correct to derive implementations.

@joneshf
Copy link
Member

joneshf commented Apr 12, 2016

It means you don't need to actually write the specific implementation for your data type. You can use that derived version that is general to ALL Applicatives. How you use that derived version is up to you. You can just pull it in from somewhere it's already written. So something like:

// Throw this in some library on npm.
function mapFromApplicative(f) {
  return this.of(f).ap(this);
}
// This is your application code.
function Identity(x) {
  this.map = mapFromApplicative;
  this.ap = function(other) {
    return new Identity(x(other.runIdentity));
  };
  this.of = function(y) {
    return new Identity(y);
  };
  this.runIdentity = x;
}

Or you can use a library that takes into account the full spec and provides this derivation for you if you don't write it yourself. So you'd do something like:

// This would be similar to https://github.com/fantasyland/fantasy-sorcery/blob/287304318f4f21f5ec2dc2c269a9924a8748a866/index.js#L16-L21
// But it would check for the `Applicative` case as well.
const someBadAssLib = require('someBadAssLib');

function Identity(x) {
  this.ap = function(other) {
    return new Identity(x(other.runIdentity));
  };
  this.of = function(y) {
    return new Identity(y);
  };
  this.runIdentity = x;
}

someBadAssLib.map(x => x + 10, new Identity(3));

@joneshf joneshf closed this as completed Apr 12, 2016
@davidchambers
Copy link
Member Author

After reading the first half of your comment I was able to resolve the contradiction in my mind. For a data type to satisfy the Functor specification it must provide a (compliant) map method, but one may define this method via mapFromApplicative. Great. Makes sense.

The second half of your comment reintroduces the contradiction. You suggest that someBadAssLib.map could operate on a value which does not have a map method and therefore does not satisfy the Functor specification. But surely a function of type Functor f => (a -> b) -> f a -> f b requires a functor?

Your interpretation of the spec appears to be that to satisfy the Functor specification a value must:

  • provide a compliant map method; or
  • provide a compliant of method and a compliant ap method.

If this is how the spec is intended to be interpreted, we should:

  • clarify the spec; and
  • encourage library authors to provide derived implementations (currently R.map, for example, does not support all functors since those with of and ap but without map are not supported).

@CrossEye
Copy link

Certainly a close reading of the spec makes this feel contradictory.

Without a compiler to supply the missing map method when it's not supplied, it's challenging to say that one can skip supplying it. But if the tools are written to support it, doing so would be completely reasonable. As David points out, Ramda could well decide to map against those objects with ap and of. But if I pass my FancyApplicative to some tool expecting a Functor, whose fault is it when that tool tries to call map and there isn't one there?

@joneshf
Copy link
Member

joneshf commented Apr 12, 2016

The second half of your comment reintroduces the contradiction.

The first half of my reply was meant to be the actual answer. The second half is just some interesting thing you can do.

encourage library authors to provide derived implementations (currently R.map, for example, does not support all functors since those with of and ap but without map are not supported).

But if I pass my FancyApplicative to some tool expecting a Functor, whose fault is it when that tool tries to call map and there isn't one there?

We shouldn't push the burden to support this derivation on library authors though. If they want to, great. But they can also just blow up if you don't provide the correct function. map in particular can come from many places: Applicative, Monad, Traversable, Comonad, etc.

Should ramda have to check at least those four different derivations (and more once the spec grows) because some person is too lazy to write a couple of lines? That seems unnecessary. If ramda wants to, there's nothing to stop it. But to make it a requirement is too much in my book.

@joneshf joneshf reopened this Apr 12, 2016
@rpominov
Copy link
Member

In Static Land I have a deriveAll function that takes an incomplete type and returns a new type with everything that can be derived added. This is easy in SL because what is "type" is well defined there. In FL it's a bit harder to have something like this because a FL type can use prototypes or factory pattern etc.

In SL there probably will be a requirement that when you create a type you must apply deriveAll to it.

@CrossEye
Copy link

But if I pass my FancyApplicative to some tool expecting a Functor, whose fault is it when that tool tries to call map and there isn't one there?

We shouldn't push the burden to support this derivation on library authors though. If they want to, great. But they can also just blow up if you don't provide the correct function. map in particular can come from many places: Applicative, Monad, Traversable, Comonad, etc.

Hmmm.

Are you saying that this:

A value which satisfies the specification of an Applicative does not need to implement:

Functor's map; derivable as function(f) { return this.of(f).ap(this); }

does not actually mean that your spec-conforming type may skip implementing map if it wants to claim to be an Applicative, but only that it might choose to implement it via of and ap? If so, I think the spec is quite confusing.

@joneshf
Copy link
Member

joneshf commented Apr 13, 2016

I'm very confused by your question.

@CrossEye
Copy link

Sorry, been offline a few days.

And yes, that was poorly worded.

To be an Applicative, does a type actually have to implement map in some manner. That is, it I hand you an instance of my type, which I claim is an Applicative, should you be able to find a map method on it? Or is there some understanding that since it already has of and ap, I don't actually have to have map on it? This, I believe, is the point David has been making, and I think it's a very good one. Different statements in the spec seem to offer different answers.

@calvinlfer
Copy link

calvinlfer commented Apr 15, 2016

Bit of an outside perspective. In Functional Programming in Scala (basis for ScalaZ and Cats), they keep map2 as the primitive combinator and map (derived combinator) is defined in terms of map2 since they define an Applicative to be a Functor.

Where map2 is referred to as liftA2

Here is their interface
image

So I believe you do need to find apply/ap, unit, map and map2/liftA2 in order to be called an Applicative. Do you guys agree with that?

@joneshf
Copy link
Member

joneshf commented Apr 16, 2016

To be an Applicative, does a type actually have to implement map in some manner. That is, it I hand you an instance of my type, which I claim is an Applicative, should you be able to find a map method on it? Or is there some understanding that since it already has of and ap, I don't actually have to have map on it? This, I believe, is the point David has been making, and I think it's a very good one. Different statements in the spec seem to offer different answers.

Per the spec https://github.com/fantasyland/fantasy-land/blob/885146b75fb055f84eccb20db6dacbb8050f6bc5/README.md#general, which I did not know existed in my previous replies, If you implement Applicative you do not HAVE to implement map.

@joneshf
Copy link
Member

joneshf commented Apr 16, 2016

So I believe you do need to find apply/ap, unit, map and map2/liftA2 in order to be called an Applicative. Do you guys agree with that?

As your picture shows, you only need unit and one of either apply or map2. The missing functions can be derived.

@rpominov
Copy link
Member

rpominov commented Apr 16, 2016

IMO, it would be easier for everybody if the spec would require that all methods that a type claims to support must be added to the type. It's not a big deal for the person who implements a type. They can easily add the method by copy-pasting code from the spec.

@calvinlfer
Copy link

calvinlfer commented Apr 16, 2016

@joneshf absolutely, you can implement the core combinators and have the derived combinators do the rest of the work. However, the end result should net you all the functions I mentioned. That's what I meant to say. Thanks for pointing that out

@CrossEye
Copy link

Somehow I lost the long reply I'd written here. I don't have the heart to type it again at the moment. The basic gist was that if the interpretation from @joneshf is right, the burden on libraries that want to work generically with these types is fairly large. I have to implement map as a call to the map method of a Functor, but also via its derivation from chain and of, or through ap and of, and wait, that ap might itself be derivable from chain and ... map? Hmmm.

I agreed with this from @rpominov:

IMO, it would be easier for everybody if the spec would require that all methods that a type claims to support must be added to the type.

and wondered if this whole "can be derived from" bit should simply not be ported over from compiled languages to JS.

@rpominov
Copy link
Member

and wondered if this whole "can be derived from" bit should simply not be ported over from compiled languages to JS.

I think this should be in the spec but with somewhat different meaning. Derived methods add restrictions on how methods can be implemented, and I think this should be explicitly stated in the spec:

  • If a method can be derived, it must be lawful. (Or is it lawful automatically if source methods were lawful? I think I have an example where we get unlawful ap from lawful chain)
  • If a method can be derived and the type also has a hand written version of that method, derived and hand written methods must be equivalent.
  • If two derived version of a method can be created (for instance we can derive map using ap or chain), they must be equivalent.

@CrossEye
Copy link

I think this should be in the spec but with somewhat different meaning. [ .... ]

Yes, I could certainly buy that.

@joneshf
Copy link
Member

joneshf commented Apr 16, 2016

I have to implement map as a call to the map method of a Functor, but also via its derivation from chain and of, or through ap and of, and wait, that ap might itself be derivable from chain and ... map? Hmmm.

You don't have to implement anything in ramda, nor would @davidchambers have to implement anything in sancturary, etc. That work can be offloaded to a spec compliant helper library. That is the point of this generality. Only one library really needs to exist that does the derivation. And it can be used for ANY data type.

In actuality, the derivations could be provided by this repo. So you'd only have one dependency, and it would be tied to the spec as well.

@joneshf
Copy link
Member

joneshf commented Apr 16, 2016

@joneshf absolutely, you can implement the core combinators and have the derived combinators do the rest of the work. However, the end result should net you all the functions I mentioned. That's what I meant to say. Thanks for pointing that out

Ah, makes sense. Sorry for misunderstanding.

@rpominov
Copy link
Member

That work can be offloaded to a spec compliant helper library. That is the point of this generality.

But will it be convenient to always use this lib on method call side? I mean do we have to do something like Lib.getMap(a)(f) instead of just a.map(f) every time we want to call a FL method?

Wouldn't it be better if type implementer would do MyType.proptotype.map = Lib.defaultMap once, and we wouldn't have to care about it when using methods?

@CrossEye
Copy link

CrossEye commented Apr 18, 2016

In actuality, the derivations could be provided by this repo. So you'd only have one dependency, and it would be tied to the spec as well.

I would be very opposed to a specification also serving as a de facto necessary tool in implementing the spec. A little too much tail-swallowing for me.

But there's a major question, if @davidchambers is not right about there being some ambiguity in the specification: Such a tool is actually quite central to using the spec in a generic manner. So why hasn't it been created yet? The spec as been out for three years. Has anyone seen one in the wild?

Not having seen one makes me believe that most implementors are thinking of the spec the way @rpominov is suggesting: if I don't want to customize map for my type, I will nonetheless expose one directly through the alternate mechanisms allowed by the spec. I wouldn't think of skipping map altogether. Has anyone seen any implementations to the contrary?

@joneshf
Copy link
Member

joneshf commented Apr 19, 2016

As mentioned earlier: #132 (comment) This library has existed for the past three years: https://github.com/fantasyland/fantasy-sorcery

@joneshf
Copy link
Member

joneshf commented Apr 19, 2016

I would be very opposed to a specification also serving as a de facto necessary tool in implementing the spec. A little too much tail-swallowing for me.

It's not a requirement that you use a library like fantasy-sorcery anymore than it would be a requirement that you use already written derivations. It's just an option.

If someone wants to reimplement all this stuff they can do so. But one of the purposes of this spec is to remove duplication.

@CrossEye
Copy link

Ahh, fantasy-sorcery. I had never looked at that. Not even when you mentioned it before (who reads source code comments?!, I had absolutely no idea. To me, this spec has always been the only important thing on this site, and I know you've been urging people to look at other repos here. I have, in fact, looked at several of the types here, but hadn't seen that library at all.

Ramda might well choose to redo it on its own, as it has so far avoided all external dependencies, but that may have to change as FL grows up in any case, so we'll see.

@davidchambers
Copy link
Member Author

@puffnfresh, could you state definitively how to resolve the contradiction? The rest of us are guessing.

@rjmk
Copy link
Contributor

rjmk commented May 12, 2016

Continuing studies in @puffnfresh exegesis: #92 (comment)

cc/ @davidchambers

@puffnfresh
Copy link
Member

@davidchambers clarify the spec 👍

@davidchambers
Copy link
Member Author

I've made my best effort to do so in #134, @puffnfresh. What do you think?

Your position still isn't clear to me. If you'd like to remove

A value that implements the Apply specification must also implement the Functor specification.

and similar sentences from the spec, I'll open another pull request to do so. I would much prefer to keep this sentence and to remove the sentence which contradicts it (as in #134). A data type should provide all the appropriate methods. If it does not, a user of a data type which claims to be a functor cannot write:

Identity(42).map(f);

Instead, she must:

//    id :: Identity Number
const id = Identity(42);

typeof id.of === 'function' && typeof id.ap === 'function' ?
  id.of(f).ap(id) :
typeof Id.of === 'function' && typeof id.ap === 'function' ?
  Id.of(f).ap(id) :
typeof id.of === 'function' && typeof id.chain === 'function' ?
  id.chain(x => id.of(f(x))) :
typeof Id.of === 'function' && typeof id.chain === 'function' ?
  id.chain(x => Id.of(f(x))) :
// else
  id.map(f);

This is intolerable as far as I'm concerned. I hope you agree. :)

If we allow derivations, we must discourage the direct use of Fantasy Land methods, and encourage the use of libraries which provide derivation-aware map functions. Otherwise, instead of writing functions forall f :: Functor we'll end up writing functions which can operate only on a subset of functors.

Things will be much simpler if we remove the exceptions. Authors of data type libraries will be forced to provide the derivable methods, but I argue that they do so anyway to allow their libraries to be usable stand-alone. Furthermore, these authors are free to derive these methods, perhaps even by importing a library which provides FL derivations. The fantasy-land package itself could even provide derivations:

In actuality, the derivations could be provided by this repo. So you'd only have one dependency, and it would be tied to the spec as well.

@rjmk
Copy link
Contributor

rjmk commented May 13, 2016

If you'd like to remove

A value that implements the Apply specification must also implement the Functor specification.

and similar sentences from the spec, I'll open another pull request to do so

If that is the interpretation taken, I would recommend supplementing these sentences rather than removing them. So they would read something like:

A value that implements the Applicative specification must also implement the Apply specification, apart from implementing Functor's map

@davidchambers
Copy link
Member Author

That sounds good to me, @rjmk, if we take that path. It'd still be simpler not to have these exceptions. Hopefully Brian agrees. :)

@puffnfresh
Copy link
Member

👍

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

No branches or pull requests

7 participants