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

apis not reasonable #181

Closed
oopdafp opened this issue Oct 4, 2016 · 39 comments
Closed

apis not reasonable #181

oopdafp opened this issue Oct 4, 2016 · 39 comments

Comments

@oopdafp
Copy link

oopdafp commented Oct 4, 2016

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.

@oopdafp
Copy link
Author

oopdafp commented Oct 4, 2016

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.

@davidchambers
Copy link
Member

I'm pretty confused by much of what you wrote, @oopdafp. Sorry!

With the change of the ap function, 'pretty' is favored over readability.

Could you provide some examples which show how the ap change harms readability?

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.

What is f(x)? The application of f to x, no? Perhaps I'm missing the point, though. Are you referring to function application or the use of the fantasy-land/ap method?

@rpominov
Copy link
Member

rpominov commented Oct 4, 2016

I also didn't understand most of it to be honest.

In functional programming, functions take in values; values do not take in functions.

I think I've understood this argument and will try to answer. As I understand it, the problem is that of(x).ap(of(f)) reads as x apply f which sounds wrong, as if we are applying x to f. Yet I wouldn't worry too much about how things sound in this particular case. It's like when we do (+ 1 2) in LISP. This doesn't really matter IMO.

We have the same problem with map, BTW. In of(x).map(f) would be better if f went first somehow and we could say "map f over of(x)".

@oopdafp
Copy link
Author

oopdafp commented Oct 4, 2016

Sorry! I will try again -

Could you provide some examples which show how the ap change harms readability?

We read left to right

[operation] [arguments] [return]

While map gives a return value, apply does not necessarily correspond 1:1 with map.

What is f(x)? The application of f to x, no? Perhaps I'm missing the point, though. Are you referring to function application or the use of the fantasy-land/ap method?

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 also didn't understand most of it to be honest.

I tried to show the difference between map and apply, and how order matters for apply.

Yet I wouldn't worry too much about how things sound in this particular case. It's like when we do (+ 1 2) in LISP. This doesn't really matter IMO.

We write code for humans, not for machines. Lisp is a scripting language, has broken semvar countless times. I will come back to this.

We have the same problem with map

I tried to portray how map is different from apply, and how apply order does matter.

Map, in linguist:

"Value mapped over by fn"

Apply in linguist:

"Value applied to fn"

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).

@oopdafp
Copy link
Author

oopdafp commented Oct 4, 2016

Here are fundamentals on

map
and
apply

(The basis you are re-defining)

@bergus
Copy link
Contributor

bergus commented Oct 4, 2016

@davidchambers

Could you provide some examples which show how the ap change harms readability?

Have a look at the example here.

@oopdafp

Apply in linguist:

"Value applied to fn"

Yeah, that sounds weird. It should be "fn applied to value" or "apply fn to value".

@oopdafp
Copy link
Author

oopdafp commented Oct 4, 2016

@bergus

@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

@rpominov
Copy link
Member

rpominov commented Oct 4, 2016

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 ap method has nothing to do really with Function.prototype.apply.

@oopdafp
Copy link
Author

oopdafp commented Oct 5, 2016

@rpominov

Fair enough, thanks!

@oopdafp oopdafp closed this as completed Oct 5, 2016
@oopdafp oopdafp reopened this Oct 5, 2016
@oopdafp
Copy link
Author

oopdafp commented Oct 5, 2016

On second thought, that is not exactly the point of this issue - merely a point of reference from academia

@evilsoft
Copy link
Contributor

evilsoft commented Oct 5, 2016

I do agree with you, about it looking horrible with the new spec.
In the old spec my liftA3 function looks like:

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 ap flows.

The main concern I ran into is with the statement given a.ap(b): "if b does not wrap a function, then the behavior is unspecified". Why I see that a concern is that because I was satisfying that in requirement in a before, I did not need to extract from the foreign container inside of a. I could check what the "home" container was wrapping. For my Either it looks like this with the old spec:

  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 m.map to never have to extract the "foreign" container.

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 ap and not chain I would need to replace the above if to reference m.either and swap the checks around, which again is not too ugly, BUT I find it gross to report errors about the "foreign" container other than type mismatches. To provide errors in the "home" container about a value in a "foreign" container seems like living in a Police State.

But these things are just my issues with it and I am probably doing something wrong 😸

I was planning on adding an apValue to my containers to use it all pretty like with out silly extraction. But once I got through my Either, I was like...I am staying at 0.x. and just going to add bits like bimap and traverse (and keeping sequence), but implement all other bits in the 0.x spec.

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.

@oopdafp
Copy link
Author

oopdafp commented Oct 5, 2016

@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

@evilsoft
Copy link
Contributor

evilsoft commented Oct 5, 2016

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 😸

@evilsoft
Copy link
Contributor

evilsoft commented Oct 5, 2016

I just saw this issue as an opportunity to get my 2 cents in! 🍔

@evilsoft
Copy link
Contributor

evilsoft commented Oct 5, 2016

Even though Data.Task is an invalid implementation of ap I will be gosh darned if I do not love the fact that I can get Promise.all functionality by doing a traversal of Task on a List of string paths. And they all run Async!!

But even-though that is AWESOME, it still violates the law

@davidchambers
Copy link
Member

@evilsoft, if you're interested in run-time type checking have a look at sanctuary-js/sanctuary#216. Here are some S.ap usage examples:

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’.

@joneshf
Copy link
Member

joneshf commented Oct 5, 2016

Let me color the discussion a bit more.

@oopdafp as you point out with #50, previously, you couldn't type ap in flow or TypeScript. Their type systems aren't powerful enough to deal with it. Part of the reason we moved to this current way was because it was impossible for some js devs to use ap with the state of the art in js.

We weren't intending to alienate people. We were intending to include more people.

@evilsoft
Copy link
Contributor

evilsoft commented Oct 5, 2016

So @joneshf part of this change was to accommodate some libs that may or may not be around?
I was hoping that FL would be around longer then the like of Typescript or Flow.
Don't get me wrong, I love me some types, but the "modern" JS libs are totally not the right way to go for types in JS (IMO). I totally see another CoffeeScript situation, when a much better solution hits JS.

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.

@evilsoft
Copy link
Contributor

evilsoft commented Oct 5, 2016

@davidchambers Oh yeah I totes ❤️ Sanctuary....Recommend to to people all the time.
But I was implementing for my own lib I am making.

EDIT: Oh my my my, DAT PR is insane!!! Will dig in.

@rpominov
Copy link
Member

rpominov commented Oct 5, 2016

@evilsoft

Even though Data.Task is an invalid implementation of ap I will be gosh darned if I do not love the fact that I can get Promise.all functionality by doing a traversal of Task on a List of string paths. And they all run Async!!

This seems like a separate issue, take a look at #179

@evilsoft
Copy link
Contributor

evilsoft commented Oct 5, 2016

@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!
But gosh I 💓 that feature of Data.Task, even though it is not sequential in its execution. Even though it breaks the rules, it provides so much value in doing so.

@ghost
Copy link

ghost commented Oct 8, 2016

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

@davidchambers
Copy link
Member

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

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 Maybe (a -> b) is Nothing there is—as you rightly stated—no function to apply, so the result is Nothing. This should not result in an exception. Perhaps you're using a data type with the new ap in conjunction with Ramda, which is not yet compatible with [email protected].

@ghost
Copy link

ghost commented Oct 8, 2016

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 apshould be changed to

...
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 m passed to an Just instance's .ap happens to be a Nothing, the .map call on that instance (also called by the traverse internals) would call

...
  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.

@davidchambers
Copy link
Member

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;
};

In order to conform to the new spec the ap should be changed to […] right?

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.

@ghost
Copy link

ghost commented Oct 9, 2016

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.

@davidchambers
Copy link
Member

I was trying to make thing kind of type agnostic.

The implementation of fantasy-land/ap will be specific to the data type, but users of the data type are able to program to a principled interface. FL ultimately exists for the benefit of users rather than authors of ADT libraries. ;)

@evilsoft
Copy link
Contributor

@davidchambers IMO, that implementation that you provided for Maybe.prototype.ap seems a little odd. I typically do mine more like this:

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 map it and let map take care of it.

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 x => Maybe(add(3)).ap(Maybe(x)) just trying to keep it easy to follow.)

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 ap bits themselves.

From an ADT lib dev perspective, I am forced to extract and do operations on a foreign Apply
From an end-user perspective, I loose the ability to choose between point-wise or point-free implementation and am forced into a point-wise interface.

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 ap code will have to be seriously reworked from the end user perspective. Going to be a bumpy transition indeed.

Specs are hard 😸

@joneshf
Copy link
Member

joneshf commented Oct 10, 2016

As an aside, your data type you posted up there is not a Functor: #85 (comment)

@joneshf
Copy link
Member

joneshf commented Oct 10, 2016

So @joneshf part of this change was to accommodate some libs that may or may not be around?
I was hoping that FL would be around longer then the like of Typescript or Flow.
Don't get me wrong, I love me some types, but the "modern" JS libs are totally not the right way to go for types in JS (IMO). I totally see another CoffeeScript situation, when a much better solution hits JS.

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.

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.

@evilsoft
Copy link
Contributor

evilsoft commented Oct 10, 2016

As an aside, your data type you posted up there is not a Functor: #85 (comment)

@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:
https://github.com/evilsoft/crocks/blob/master/crocks/Maybe.js

Do you still see any composition or identity issues in context?

EDIT: can only get at the value with: maybe, option, or either
ANOTHER EDIT:

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:
Not trying to use the factory to hide implementation details from a user or encapsulation or anything like that. It is just to make a consistent API where all operations on ADTs are done through functions.

OH MY YET ANOTHER:
Nope totes not a Functor, should be compose(g, f) which of course is Just(10)...need to fix that. Thanks so much for the find!!!

@davidchambers
Copy link
Member

Your Maybe type is invalid, @evilsoft. It doesn't permit Just(null) and Just(undefined).

I am curious about what you mean by the users of FL.

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.

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)
}

Why use ap rather than map? This could simply be map(add(3)), I believe. I may not be the best person to comment on this, though, as I like to use R/S/Z functions rather than invoke Fantasy Land methods directly.

@evilsoft
Copy link
Contributor

@davidchambers:

Your Maybe type is invalid, @evilsoft. It doesn't permit Just(null) and Just(undefined).

You just blew my mind! both you and @joneshf are totally correct! Wish I made a fool of myself a couple months ago...

Why use ap rather than map? This could simply be map(add(3)), I believe. I may not be the best person to comment on this, though, as I like to use R/S/Z functions rather than invoke Fantasy Land methods directly.

💯 on that. It is a total strawman, but just wanted to provide an example, I think I commented on the, .map(add(3))

@evilsoft
Copy link
Contributor

@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"

@evilsoft
Copy link
Contributor

Yep. I see now. That whole Maybe type is bogus...all over the place.
But TIL what the phrase

no part of b should be checked

means and how important that is!
(Still prefer the old style of .ap even though my example was 💩, 😜)

@davidchambers
Copy link
Member

Still prefer the old style of .ap

Just a thought, @evilsoft: you're free to define ap any way you please now that we have prefixed names. If you intend users of your ADT to use the object-oriented interface directly you could provide an ap method which works as you prefer (in addition to providing a compliant fantasy-land/ap method).

@SimonRichardson
Copy link
Member

I think we should close this, although this is a good discussion, it's not going anywhere... If anyone disagrees just reopen...

@oopdafp
Copy link
Author

oopdafp commented Oct 11, 2016

Sorry to show up late, I've been away for a minute - such a discussion!

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;
};

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?

@davidchambers
Copy link
Member

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

I'm not sure what you're asking, @oopdafp. Are you asking how I arrived at the type signature of Maybe#fantasy-land/ap? If so, I can answer your question. Start with the general type:

ap :: Apply f => f a ~> f (a -> b) -> f b

Replace Apply f => f with Maybe:

ap :: Maybe a ~> Maybe (a -> b) -> Maybe b

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