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

Question about Applicatives #50

Closed
cwmyers opened this issue Oct 26, 2013 · 23 comments
Closed

Question about Applicatives #50

cwmyers opened this issue Oct 26, 2013 · 23 comments
Labels

Comments

@cwmyers
Copy link

cwmyers commented Oct 26, 2013

Hi guys,

I've been implementing FantasyLand in monet.js and I've come a little unstuck with ap().

Unless I've got the wrong end of the stick FantasyLand's ap() demands that the applicative already contains a function and then accepts an applicative with a value.

appWithFunction.ap(appWithValue)

Unfortunately I've followed scalaz & functional java approach which does the opposite.

appWithValue.ap(appWithFunction)

See here for the Maybe example: http://cwmyers.github.io/monet.js/#apmaybefn

Here is an example from FunctionalJava Option:
public final <B> Option<B> apply(Option<F<A,B>> of)

So my question is why has Fantasy Land taken this particular approach to applicative? I'm worried that this would be a large breaking change to the monet library and I'm trying to figure out what the right thing is to do.

Cheers,
Chris

@puffnfresh
Copy link
Member

I guess I copied Haskell's <*> infix operator. Just like <$> (i.e. infix fmap) - it's flipped to what it probably should be.

It'd probably be more consistent to flip it (i.e. it would look more like other functors).

What should we do about updating the specification? I have no hesitation to release new versions of my projects but does anyone have a project where they can't or don't want to change the order of ap?

@joneshf
Copy link
Member

joneshf commented Nov 4, 2013

I think this goes back to #24. However, we haven't really been keeping up with updating the version number to begin with. If you'd like I can go back to when version 0.0.1 was first "released" and look through the changes since then, tagging and whatever. Or we can just start right now with a set version, and keep everything semver'd from here on.

@jneen
Copy link
Contributor

jneen commented Jan 21, 2014

There's an alternative definition of Applicatives that uses a "join" or "seq" operator instead of "ap".
Here's how join (unrelated to join :: m (m a) -> m a) would look (note: code in this comment is pseudo-haskell):

class Functor f => Joinable f where
  pure :: a -> f a
  join :: f a -> f b -> f (a, b)

It's equivalent to ap:

join u v = ap (map (,) u) v
ap u v = map (\(f, x) -> f x) (join u v)

And it even comes with equivalent laws:

join (pure x) v = map (x,) v
join v (pure x) = map (,y) v
join (map f u) (map g v) = map (f *** g) (join u v)
join u (join v w) = map (\(x, (y, z)) -> ((x, y), z)) (join (join u v) w)
map f (pure x) = point (f x)

The "ap"-based one seems to be the best in Haskell since functions are auto-curried and tuples are non-recursive. But in JS we can use heterogenous lists instead, so "join" might be more natural to express. In fantasyland, I guess it would be something like "a.join(functors)" is a functor of a combined array of results from the given functors.

Anyways, slightly rambly, but this represents the way the seq method is used in Parsimmon, which is much more naturally usable than ap (in fact, Parsimmon's ap is implemented in terms of seq). It'd be worth considering whether to standardize this sort of interface, since I can imagine it coming up quite a lot.

@jneen
Copy link
Contributor

jneen commented Jan 21, 2014

related to #56, although liftN still requires a function.

@bergus
Copy link
Contributor

bergus commented Mar 9, 2014

appWithValue.ap(appWithFunction)

looks really odd to me. Applying functions with multiple arguments doesn't seem to work well - or is quite ugly, if we look the linked example:

var person = function(forename, surname, address){…}.curry()
var maybePersonResult = maybeAddress
                        .ap(maybeSurname
                            .ap(maybeForename
                                .map(person)))

Not only is there much nesting, but also the order of the arguments is inverted. Having used .map() instead of .ap(Maybe.of()) might save characters and an explicit Maybe reference, but doesn't make it look better.

The appWithFunction.ap(appWithValue) order is better imho:

Maybe.of(person).ap(maybeForename).ap(maybeSurename).ap(maybeAddress)
// or even
Maybe.of(person).ap(maybeForename, maybeSurename, maybeAddress)

It wouldn't be more consistent to flip it just because we have flipped fmap to become a .map method of a Functor instance - Applicatives are more than Functors and .ap can already be a method of an Applicative instance.

@jneen
Copy link
Contributor

jneen commented Mar 9, 2014

In Parsimmon I've recently gone with the join implementation I mentioned here, although I renamed it to seq to match Parsnip. Basically, seq :: [Parser *a] -> Parser [*a] where *a is heterogenous. ap is implemented in terms of this primitive.

@puffnfresh
Copy link
Member

Yeah, this needs to be fixed. PR would be awesome!

@SimonRichardson
Copy link
Member

Should we flip the order of ap, or just live with it?

@rpominov
Copy link
Member

I'm having trouble typing ap() with Flow, ended up with this:

class Stream<T> {

  // Apply
  ap<A,B>( other:Stream<A> ): Stream<B> {
    // In order for .ap() to work `this` must be a `Stream<A => B>`,
    // but I don't know how to express that constraint in Flow, so just use `any`
    const streamF:BasicStream<any> = this.observe
    return new Stream(bs.ap(streamF)(other.observe))
  }

}

Would be straightforward if it was other way around.

@joneshf
Copy link
Member

joneshf commented Jan 21, 2016

That is quite unfortunate. Can you actually write the other way with flow?

@rpominov
Copy link
Member

Sure, with reversed version of ap it will look like this:

class Stream<T> {

  ap<A>( other:Stream<( x:T ) => A> ): Stream<A> {
    return new Stream(bs.ap(other.observe)(this.observe))
  }

}

@rpominov
Copy link
Member

For the record: not a big problem for me, just wanted to add my two cents :)

@SimonRichardson
Copy link
Member

I've been hit with this recently as well. So the question is how do we change it, because I think we should!

Could we maybe rename ap to something else, or do we say if you depend on the new fantasy-land #92 ap works like this now?

@rpominov
Copy link
Member

We could certainly use NPM to help us manage changes like that after libraries start depending on FL.

We could do for example:

  • 1.0 backward compatibility release with unprefixed names and no changes to the spec
  • 2.0 adds prefixes therefore providing clashes safety, but still no changes to the spec
  • 3.0 no changes in method names, but includes changes to spec like this and Why can of be defined on the constructor? #88

Then libraries could specify appropriate versions range in their peerDependency to fantasy-land, and consumers won't be able to use two incompatible libraries (they won't be able to install version of fantasy-land that satisfies peerDependency for all of incompatible libs).

@SimonRichardson
Copy link
Member

I'd rather have 3.0 out before 2.0 💃

@rpominov
Copy link
Member

No preference on that matter from me :)
We could also squash 3.0 and 2.0 to one version, btw.

@bergus
Copy link
Contributor

bergus commented Jul 25, 2016

So you only want to change this because flow's type system is not sophisticatd enough?

@rpominov
Copy link
Member

rpominov commented Jul 26, 2016

Yep, and also the fact that it's inconsistent with map and chain and was a mistake from the beginning (see #50 (comment))

Feel free to discuss in the PR: #145

@Avaq
Copy link
Contributor

Avaq commented Sep 15, 2016

I think this can be closed now, no?

@SimonRichardson
Copy link
Member

Indeed it can!

@char0n
Copy link
Contributor

char0n commented Apr 17, 2017

Sorry for commenting on closed issue, but It may help somebody dealing with the same issue as I did. I heavily use monet.js algebraic structures along with ramda. As @cwmyers pointed out, applicatives in monet.js are not compatible with fantasy-land spec. Though one cannot use ramda's liftN to work with monet's applicates. I implemented liftFN to create interop for monet.js and ramda. It is part of ramda-adjunct and you can find more information in the docs.

@evilsoft
Copy link
Contributor

@char0n looks like ramda#1900 was merged. So soon, very soon, once ramda releases its next version we should all be gtg!!

@char0n
Copy link
Contributor

char0n commented Apr 17, 2017

@evilsoft I've just tried using lift from master branch of ramda, but it still doesn't work with monet.js algebraic structures. liftFN from ramda-adjunct still working. I am still investigating the issue...

I have one additional question regarding composition of Apply spec regarding v.ap(u.ap(a.map(f => g => x => f(g(x))))) is equivalent to v.ap(u).ap(a) (composition)

const add3 = curry((a, b, c) => a + b + c);

const m10 = Maybe.Some(10);
const m15 = Maybe.Some(15);
const m17 = Maybe.Some(17);

m10.ap(m15.ap(m17.map(add3)))

What is the eqvivalent of v.ap(u).ap(a) in my real code example ? Is this type of composition still possible when the ap argument must be apply of a function ?

Update:
monet.js PR #112 makes monet.js compatible with fantasy-land spec and next version of ramda (>0.23.0). Use [ramda-adjunct](https://github.com/char0n/ramda-adjunct} and liftFN for time being.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

10 participants