-
Notifications
You must be signed in to change notification settings - Fork 376
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
ap
is not reasonable
#181
Comments
I am appending to my last comment of false definitions: In functional programming, functions take in values; values do not take in functions. Please reason about your new definition of apply in comparison to prove how it is more reasonable to apply a function to a value than applying a value to a function. |
I'm pretty confused by much of what you wrote, @oopdafp. Sorry!
Could you provide some examples which show how the
What is |
I also didn't understand most of it to be honest.
I think I've understood this argument and will try to answer. As I understand it, the problem is that We have the same problem with |
Sorry! I will try again -
We read left to right [operation] [arguments] [return] While map gives a return value, apply does not necessarily correspond 1:1 with map.
Actually, f(x) == y. f(x) has no value, unless you mean fn(x) then again it is a shorthand for function(x). I am not sure if pseudocode semantics is a ground for reasoning.
I tried to show the difference between map and apply, and how order matters for apply.
We write code for humans, not for machines. Lisp is a scripting language, has broken semvar countless times. I will come back to this.
I tried to portray how map is different from apply, and how apply order does matter. Map, in linguist:
Apply in linguist:
As the argument above, functions take values, values do not take functions. With the argument (+ 1 2) in LISP, you are referring to a prefixed operation, Relative to the applicative a.ap(fn) as fn(a). |
Have a look at the example here.
Yeah, that sounds weird. It should be "fn applied to value" or "apply fn to value". |
@rpominov is correct, the way something sounds is not a valid premise. I only argue that it is an invalid take on apply because of the basis: Specs for JavaScript algebra structures. Mozilla has been doing some great documentations on the language. Haskell is not JavaScript (looking at the reason why it was swapped in the first place seems to point towards mirroring Haskell, which if you've written in Haskell might know that apply is hardly used because functions can't have types). @davidchambers Can you show where apply is reasonable in the context of value.ap(fn), maybe that will clear up confusion |
Just want to clarify that Fantasy Land doesn't take inspiration in any JavaScript specifications. The purpose of this spec is rather to specify how to apply research done in languages like Haskell to JavaScript. So the |
Fair enough, thanks! |
On second thought, that is not exactly the point of this issue - merely a point of reference from academia |
I do agree with you, about it looking horrible with the new spec. function liftA3(fn, m1, m2, m3) {
return m1
.map(fn) // Lift and apply that function
.ap(m2) // Apply dat second feller
.ap(m3) // Why not one more and then it is all lifted.
} now with the new bits my liftA3 would be: function liftA3(fn, m1, m2, m3) {
return m3.ap(m2.ap(m1.map(fn)))
} Other than looking ugly as sin (and I may be missing something here, I am a total newb after all) it is not so bad for helper functions like this. As most users will use the helper functions then composing their own The main concern I ran into is with the statement given function ap(m) {
if(!either(constant(true), isFunction)) {
throw new TypeError('Either.ap: Wrapped value must be a function')
}
else if(!either(constant(true), constant(isType(type(), m)))) {
throw new TypeError('Either.ap: Either required')
}
return chain(fn => m.map(fn))
} Every check stays inside of the "home" container, can even use Now to do those checks I need to extract the value from the foreign container also if I used the derived functionality like I am doing here in the new spec, which is: return m.chain(f => this.map(f)) In order to report the error in But these things are just my issues with it and I am probably doing something wrong 😸 I was planning on adding an The real bummer with me staying a 0.x, is that soon I will lose compatibility with Ramda, and I so wanted that, but oh well dems the breaks. I may change my mind on this and just suck it up buttercup, going to wait to see what a couple projects I care about are going to do with this. If more of them go that route, I will have no choice. Ramda was the first. but I think I can live without interop with Ramda. If two more critical libs decide to go that way I'll have no choice. Just my 2 cents on the matter. And TBH deep down I do like the 🌽sistency. I just hate the having to extract bits. |
@evilsoft that is much more of what I was looking for; not a semantic discussion but actual reasoning. @rpominov haskell is a language just like javascript, but if we copy Haskell's research to disregard JavaScript (the language which these specs are written for, as a reminder), and assuming that issue #50 was the reason for the change (which, honestly, seems like it was a trade of mutual admiration), then more reasoning is needed |
Well I am coming from the point of view as a person implementing this spec. There are some very, very good reasons for those laws. This spec is not what is the best way to implement in JS. The new spec is what it should be if you look over Control.Applicative in haskell. <*> to be precise. Even though it inconveniences me. At the end of the day, this is spec is more important then how inconvenient it is for one silly JS dev. I have been the only one I have seen in the community that has been taken aback from this change. So logic dictates I am the one with the problem 😸 |
I just saw this issue as an opportunity to get my 2 cents in! 🍔 |
Even though Data.Task is an invalid implementation of But even-though that is AWESOME, it still violates the law |
@evilsoft, if you're interested in run-time type checking have a look at sanctuary-js/sanctuary#216. Here are some S.ap([S.toUpper, S.toLower], ['Foo', 'Bar']);
// => ['FOO', 'BAR', 'foo', 'bar']
S.ap(S.Just(Math.sqrt), S.Just(9));
// => Just(3)
S.ap(S.Just('XXX'), S.Just(9));
// ! TypeError: Invalid value
//
// ap :: Apply f => f (a -> b) -> f a -> f b
// ^^^^^^^^
// 1
//
// 1) "XXX" :: String
//
// The value at position 1 is not a member of ‘a -> b’. |
Let me color the discussion a bit more. @oopdafp as you point out with #50, previously, you couldn't type We weren't intending to alienate people. We were intending to include more people. |
So @joneshf part of this change was to accommodate some libs that may or may not be around? EDIT: I mean, just the fact that it cannot accommodate this simple valid JS use case easily, should point to the assumption that TS and Flow are NOT the long term solutions. |
@davidchambers Oh yeah I totes ❤️ Sanctuary....Recommend to to people all the time. EDIT: Oh my my my, DAT PR is insane!!! Will dig in. |
@rpominov Awesome. SO much good information there. EDIT: That is exactly what I was talking about, there was a talk at one of my meetups about that!! Sweet! |
I also think that this order of arguments for .ap is not ideal. Not just it less intuititve, it also may cause problems, I just ran into one. Trying to use a version of traverse (like the one that ramda implements), with .ap implemente by the spec if, somehow the code reachs a point where the value (function) of something like Maybe.Nothing (which has no value) should be applied to the value of another structure, the code breaks because, well there is no function to be applied. The older version of .ap didn't have this sort of problem |
Argument order is actually irrelevant: > Just (* 2) <*> Just 21
Just 42
> Just (* 2) <*> Nothing
Nothing
> Nothing <*> Just 21
Nothing
> Nothing <*> Nothing
Nothing If the value of type |
I will look in more depth at this, but what I was trying to say was something like: For example, ramda-fantasy implements the Maybe monad like this ...
Just.prototype.map = function(f) {
return this.of(f(this.value));
};
Nothing.prototype.map = util.returnThis;
Just.prototype.ap = function(m) {
return m.map(this.value);
};
Nothing.prototype.ap = util.returnThis;
... In order to conform to the new spec the ...
Just.prototype.ap = function(m) {
return this.map(m.value);
};
Nothing.prototype.ap = util.returnThis;
... right? The issue I ran into was that inside the "traverse machinery" (edited to apply the Applies in the right order), if the ...
undefined(this.value)
... Well, chances are I missed something as I converted the old code to be compatible with the new spec, I looking into it. The old spec had no such problem. |
Setting aside the specifics of ramda-fantasy, the change should be straightforward. Before: // Maybe#ap :: Maybe (a -> b) ~> Maybe a -> Maybe b
Maybe.prototype.ap = function(other) {
return this.isJust && other.isJust ? Just(this.value(other.value)) : Nothing;
}; After: // Maybe#fantasy-land/ap :: Maybe a ~> Maybe (a -> b) -> Maybe b
Maybe.prototype['fantasy-land/ap'] = function(other) {
return this.isJust && other.isJust ? Just(other.value(this.value)) : Nothing;
};
That's right. Also, the method name needs to be prefixed. Perhaps you'd like to open a ramda-fantasy issue for the Fantasy Land upgrade. |
I was trying to make thing kind of type agnostic. I guess I was just being silly then. I'll test some more things and, perharps, isue a PR or something. Well, I came here to give my 2 cents to the discussion, now I think I losing the focus of the issue.. I just wanna say I agree that, even maybe less semantic, the older version of .ap was easier to digest. |
The implementation of |
@davidchambers IMO, that implementation that you provided for const K = x => _ => x
const isNothing = x => x === undefined || x === null
function Maybe(x) {
const either = (f, g) => isNothing(x) ? f() : g(x)
const option = n => either(K(n), K(x))
const map = fn => Maybe(either(K(null), fn)
const ap = m => m.map(option(K(null))
...
return { either, option, map, ap }
} Trying to avoid doing checking and what have you on the foreign types and just letting the algebras take care of it for me. Notice not ONCE do I extract or otherwise do anything with the foreign Apply, I just And I am curious about what you mean by the users of FL. I thought the you needed ADT lib authors to implement against the spec in order to get users to even be aware of FL? Who are the users that FL is targeting then? How would someone not using some ADT lib use FL? I know I would not want users of my libs losing the ability to do something like (totally contrived, but I hope it makes sense. There are like 100 different, better ways to do this, just using a simple example): const add = curry(
(x, y) => x + y
)
// safeAdd3 : Number -> Number
const safeAdd3 = compose(
option(0),
ap(Maybe(3)),
map(add),
Maybe
) With the new bit, end users are forced into a more point-wise flow: function safeAdd3 (num) {
return Maybe(3)
.ap(Maybe(num).ap(Maybe(add)))
.option(0)
} (again i KNOW it would be better to just do I mean I am just a noob at this stuff, but I think a point-free composition is a little easier to follow from an end-user, non-ADT lib developing developer perspective. So it seems to be a PITA from either user. And granted, a majority of the time, my users will be using my liftAN functions that will hide the nasty and allow them to still be point-free. But, it is just terrible if they decide that they need to From an ADT lib dev perspective, I am forced to extract and do operations on a foreign Apply I know you guys will not change it, because you have some good reasons not to. Please do not take this as a foolish attempt at rabble rousing. I am just voicing my pain points as I try to navigate through this change. It is too bad I do not use T$, that way I could get some benefit from all of this. ;) I feel sorry mostly for those end users who are going to go through the 9 planes of heck as interop between the current libs is going to be super chaotic while people make the transition. Some will be 0.x and others will not for extended periods of time, if people even decide to transition. So for a while we will lose all kinds of interop and could not safely upgrade until ALL FL deps in a project are upgraded. And even then all Specs are hard 😸 |
As an aside, your data type you posted up there is not a |
That's totally fair! Also, I don't want to give the impression that this change was totally to help TS/flow here. It was just another compelling case for the change. Hopefully, TS and flow get more powerful, so we don't need to bend to their will. Or, like you said, hopefully something better comes along that doesn't have these problems. In either case, I hope we continue to evolve the spec with the times. |
@joneshf Good call! You are 100% about that. I removed a bunch of stuff there to keep it light and tidy. I use the ADT factory function approach, so you HAVE to use one of my extraction functions to get at your value. Here is what the code looks like in full: Do you still see any composition or identity issues in context? EDIT: can only get at the value with: const { Maybe, compose, constant } = require('crocks')
const f = constant(null)
const g = constant(10)
Maybe(1).map(f).map(g).inspect() // Maybe.Nothing
Maybe(1).map(compose(f, g)).inspect() // Maybe.Nothing YET ANOTHER EDIT: OH MY YET ANOTHER: |
Your Maybe type is invalid, @evilsoft. It doesn't permit
The pay-off is getting to write parametrically polymorphic functions such as this: // sum :: Foldable f => f Number -> Number
const sum = foldable => foldable['fantasy-land/reduce']((a, b) => a + b, 0); Users of FL-compatible data types benefit from programming against a standard, principled interface.
Why use |
You just blew my mind! both you and @joneshf are totally correct! Wish I made a fool of myself a couple months ago...
💯 on that. It is a total strawman, but just wanted to provide an example, I think I commented on the, |
@davidchambers and @joneshf: well thinking on the feedback you two have given me, it seems like it all goes back to what my dear, sainted grandaddy Hicks used to say: "Ian, always remember, Union Types make everything better" |
Yep. I see now. That whole Maybe type is bogus...all over the place.
means and how important that is! |
Just a thought, @evilsoft: you're free to define |
I think we should close this, although this is a good discussion, it's not going anywhere... If anyone disagrees just reopen... |
Sorry to show up late, I've been away for a minute - such a discussion!
Could you provide reference to the 2nd definition here? I'm suctioned to say that this follows no definition of Maybe, and the first is correct :: (a -> b) -> a -> b Applying maybe instances: Maybe (a->b) -> Maybe a -> Maybe b Maybe a -> Maybe b -> Maybe b Maybe a -> Maybe b -> Maybe a Maybe [heh] i'm reading something wrong? |
I'm not sure what you're asking, @oopdafp. Are you asking how I arrived at the type signature of ap :: Apply f => f a ~> f (a -> b) -> f b Replace ap :: Maybe a ~> Maybe (a -> b) -> Maybe b |
I am writing this issue to prevent confusion to new readers.
With the change of the
ap
function, 'pretty' is favored over readability.definition of map:
A linear transformation; i.e. a -> fn(a)
definition of chain:
A derive of a composition, i.e. g(fn(a)) -> g(a).f(a)
definition of apply:
Isomorphism of a left-associative function, i.e. fn(x*y) -> fn(x,y)
The ap definition as of right now suggests right-associative, giving the reverse of what an apply does.
We may enjoy having consistent definitions, but please do not spread false definitions.
The text was updated successfully, but these errors were encountered: