fantasy-land: `ap`is not reasonable

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.

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Comments: 39 (28 by maintainers)

Most upvoted comments

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

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

As an aside, your data type you posted up there is not a Functor: https://github.com/fantasyland/fantasy-land/issues/85#issuecomment-73419882

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

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.

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

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 fantasy-land@1.x.x.

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

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

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.