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

Just an idea: Why not make LiftN the requirement for applicatives rather than ap? #56

Closed
russellmcc opened this issue Jan 9, 2014 · 7 comments

Comments

@russellmcc
Copy link

It seems like in a language like JavaScript where currying isn't the norm (and might not be performant because of a lack of compiler help), it might be more useful to make something more like LiftN the requirement for applicative rather than ap. It could even be backwards compatible for non-library code by just allowing ap to take multiple arguments like this:

MyApp.of(f).ap(a, b, c, d, e)

This would be fairly easy to implement for most applicatives I can think of, and would certainly be more convenient for the user and probably more performant, too.

If this doesn't match the aesthetics of fantasy-land (and my guess is it doesn't), maybe there should be a way for implementors to optionally provide a liftN method (under some friendlier name, maybe just lift) for performance reasons?

@SimonRichardson
Copy link
Member

Can you propose a implementation, so then we can move the discussion on. As you say I don't think we'll provide a way to overload the arguments of ap... although @puffnfresh might have a different idea?

One possible way is if you know some of the arguments before the method call is to provide a bind:

var id = require('fantasy-identities');

var f = function(a, b, c, d) {
    console.log(a, b, c, d);
}
var unshift = function(f) {
  return function() {
        // horrid state :(
        var x = [].slice.call(arguments)
        x.unshift(x.pop());
        return f.apply(null, x)
  };
}

var a = id(unshift(f).bind(null, 1, 2, 3))
console.log(a.ap(id('a')))

@russellmcc
Copy link
Author

well, the implementation would of course have to be applicative-specific. I've demoed the three suggestions proposed for 'option' (aka 'Maybe') with some examples here (apologies, I don't actually know javascript so the example is in coffeescript with auto-generated javascript).

https://gist.github.com/russellmcc/9357418

I think 'join' and 'lift' are probably the two most sensible choices for primitives (of course they are closely equivalent), but the point I was making with ap is that it's easy to extend the existing semantics to support multiple arguments.

@phadej
Copy link
Contributor

phadej commented Apr 15, 2014

I'd really like lift behaving like:

  • Value.lift(x) = Value.of(x)
  • Value.lift(f, x) = Value.of(f).ap(x) = x.map(f)
  • Value.lift(f, x, y) = Value.of(autocurry(f, 2)).ap(x).ap(y) = x.map(autocurry(f)).apply(y)
  • Value.lift(f, x, y, z) = ...

So if value has lift you can fantasy fill map, of and ap:

Value.of = Value.lift; // if you don't want to check arity
Value.prototype.map = function (f) {
  return Value.lift(f, this);
};
Value.prototype.ap = function (x) {
  return Value.lift(apply, this, x); // apply from fantasy-combinators
};

The other direction is also possible, though not so trivial

Value.lift = function (f) {
  if (arguments.length <= 1) {
    return Value.of(f);
  } else if (arguments.length == 2) {
    return arguments[1].map(f);
  } else {
    f = autocurry(f, arguments.length - 1);
    var x = arguments[1].map(f); // should we wrap this to not break on Array?
    for (var i = 2; i < arguments.length) {
      x = x.ap(arguments[i]);
    }
    return x;
  }
};

So you can derive methods like in:
methods derivability graph
seq is from #50
and I'd call monadic join squash so it doesn't clash so badly.

@joneshf
Copy link
Member

joneshf commented Apr 15, 2014

I gotta say, your graphics are awesome!

@phadej
Copy link
Contributor

phadej commented Apr 16, 2014

@joneshf, thanks, those you learn to do in the university ;)

And one addition might be to have tfil, flippled lift, so your applicative parser could be like:

function braces(par) {
  return Parser.tfil(Parser.str("("), par, Parser.str(")"), function (a, b, c) {
    return b;
  });
}

IMHO it's might more readable, and maybe "more idiomatic" JavaScript. Kind of inspired by Haskell's where, as I leave the pure combinator hanging after the applicative expression:

braces p = f <$> string "(" <*> p <*> string ")"
  where f _ x _ = x

methods

@bergus
Copy link
Contributor

bergus commented Jul 29, 2015

I think we should revisit this (I want to implement it :-)).
Making a liftA2 (or liftF2) function the alternative definition of Apply seems very attractive (would be placed at seq in @phadej's diagrams). I guess that it's more often needed than ap. Of course they can be derived from each other, but making lift is a bit ugly in JS as it requires currying.

function ap(af, ax) {
    function call(f, x) { return f(x); }
    return lift(call, af, ax);
}
function lift(f, ax, ay) {
    var f_curried = function(x) { return function(y) { return f(x, y); }; };
    return ax.map(f_curried).ap(ay);
}

I don't think we should make this a method though but rather a static function, and I cannot really envision a name for it either: ax.apWith(f, ay) sounds wrong because we're not applying ax but rather f, and I really don't want to build upon thrush or T-combinator (see this SE question for references).


Of course, a variadic liftN function might be even more useful (easier to implement and call). It's quite common in libraries, e.g. as combineWith in Bacon, or join in Bluebird. There are versions that optionally take arrays as inputs instead, and I think that's a route we could go down as well to make a proper binary function (array + fn), as we've seen that generic libraries like Ramda which use currying don't really like variadic functions. Yet after all this might fit better with Applicative than in Apply (which cannot handle the nullary case), and in this case I think we might just introduce an optional sequence function that could be used like

sequence([ax, ay, az, ]).map(function([x, y, z, ]) {
    
});

which already looks a lot like that tfil idiom that was suggested. Implementing sequence in a library is probably much more efficient than deriving it as

function sequence(arr) {
    return arr.reduce(function(ar, ax) {
        return ar.map(function(res) {
            return function(x) {
                res.push(x);
                return res;
            };
        }).ap(ax);
   }, of([]));
}

@SimonRichardson
Copy link
Member

Is this going to happen, or should I close it for now?

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

5 participants