puffnfresh / roy

Small functional language that compiles to JavaScript.
http://roy.brianmckenna.org/
MIT License
834 stars 74 forks source link

Currying #175

Open puffnfresh opened 11 years ago

puffnfresh commented 11 years ago

One of the goals for Roy is to ensure the JavaScript output should easily be used from plain JavaScript (and hopefully other languages).

Here we want f to be a curried function:

let f a b = a + b
let g = f 42

The problem with currying each function definition is that it doesn't compile to great JavaScript:

function f(a) {
    return function(b) {
        return a + b;
    };
}
var g = f(42);

But maybe we could implement currying on the caller side?

function f(a, b) {
    return a + b;
}
function g(b) {
    return f(42, b);
}

Yes, the JavaScript it not great - but only if you use the currying. Plain JavaScript clients don't change.

kmarekspartz commented 11 years ago

I would expect the following:

f (a, b) = a + b
g a b = a + b

to become:

var f = function(a, b) {
    return a + b;
};

var g = function(a) {
    return function (b) {
         return a + b;
    };
};

This would allow you to still use the tupled functions when you need to for compatibility. We can then have a functions to convert between curryable and tupled functions:

curry2 f a b = f (a, b)
curry3 f a b c = f (a, b, c)
etc.

uncurry2 f (a, b) = f a b
etc.

---->

var curry2 = function (f) {
    return function (a) {
        return function (b) {
              return f(a,b);
        };
    };
};
etc.

var uncurry2 = function (f) {
    return function (a, b) {
         return f(a)(b);
    };
};
etc.
joneshf commented 11 years ago

@pufuwozu sorry, I don't really understand that last example.

puffnfresh commented 11 years ago

@joneshf sorry, that made no sense. Just updated it with what I meant.

kmarekspartz commented 11 years ago

By this logic, lambdas could be lifted, too.

f a b = \x -> a + x

g b = f 12 b 12

--->

function f (a, b, x) {
    return a + x;
};

function g (b) {
    return f(12, b, 12);
};

But I'm not sure that is a good idea.

joneshf commented 11 years ago

@zeckalpha I kind of like the idea of a curry function, though I think we can generalize it.

var curry = function(f) {
  function currier(curriedArgs) {
    // We need at least as many args as the function takes.
    if (curriedArgs.length >= f.length) {
      // Pass them all into the function.
      // We could only pass as many as needed,
      // but some goof could be using unnamed args like this is.
      return f.apply(this, curriedArgs);
    } else {
      // Not enough args, let's curry it with what ever else comes down the pipe later
      return function() {
        var newArgs = curriedArgs.concat(Array.prototype.slice.call(arguments));
        return currier(newArgs);
      };
    }
  }
  // Grab only the arguments.
  return currier(Array.prototype.slice.call(arguments, 1));
};

Something like that, except with more clarity.

So we should be able to do things like:

let f x y = x + y
let g = f 37
console.log (g 3)

and have it compile to

var f = function(x, y) {
  return x + y;
};
var g = curry(f, 37);
console.log(g(3));

var curry = function(f) {
  function currier(curriedArgs) {
    if (curriedArgs.length >= f.length)
      return f.apply(this, curriedArgs);
    return function() {
      return currier(curriedArgs.concat(Array.prototype.slice.call(arguments)));
    };
  }
  return currier(Array.prototype.slice.call(arguments, 1));
};
kmarekspartz commented 11 years ago

Perhaps a better name for this function would be partial since it isn't currying or uncurrying, but rather using partial application and returning a closure for the remaining arguments.

joneshf commented 11 years ago

Ah, thanks for the correction.

So I guess the question is, can a generalized curry function be written that is readable? Or do we cut our losses and just say partial application is good enough? How would a plain js developer do it? Would they write the partial, or try for true currying?

kmarekspartz commented 11 years ago

I'm not really one, so I'm not sure exactly, but I don't think they would do it! The only use cases I can think of for partial application use an object passed in instead, e.g. in Underscore, the where method on collections.

rtfeldman commented 11 years ago

+1 for the original "caller side" proposal that generates the following for g:

function g(b) {
    return f(42, b);
}

If exported Roy functions are curried, calling Roy libraries from vanilla JS will entail royFunc(arg1)(arg2)(arg3) instead of the usual royFunc(arg1, arg2, arg3). Then you must remember to use that calling style only for libraries written in Roy, making Roy libraries intrinsically inconvenient to vanilla JS users.

This is an especially important consideration for people wanting to incorporate Roy into existing codebases incrementally: rewrite one piece of legacy code in Roy, then another, then another...

If Roy functions require a different calling style, then implementing drop-in rewrites of individual JS functions or modules is off the table.

kmarekspartz commented 11 years ago

Can't compatibility be managed using tupled arguments?

f (a, b) = a + b

g b = f (42, b)

puffnfresh commented 11 years ago

@zeckalpha Scala does something like that:

def f(a: (Int, Boolean), b: String) = ???
def g(a: (Int, Boolean))(b: String) = ???

(I used a tuple as an argument type just to show that you can still have that)

rtfeldman commented 11 years ago

Tupled arguments are one approach to compatibility, but it means Roy authors have to consciously code with compatibility in mind.

As a Roy author I'd rather just code naturally and have the compiler take care of generating JS-friendly code.

kmarekspartz commented 11 years ago

I like the ability to have both tupled and curried functions in ML and Haskell. I found the curried notation and tupled semantics confusing coming to Roy.

puffnfresh commented 11 years ago

@zeckalpha you can still have tupled arguments but they're not going to compile to a multiple argument JavaScript call. It'd have to be a tuple object.

(Just like Haskell)

rtfeldman commented 11 years ago

Yeah, that's how I would expect it to work from the JS side.

puffnfresh commented 11 years ago

A pollyfilled Function.prototype.bind (pollyfined for IE6) would actually be more normal to see in plain JavaScript code. That could be a good approach to "caller-side" currying.

rtfeldman commented 11 years ago

That works too. As an aside I like the idea of saying "Roy targets ES5, so include these polyfill libraries if you need to target lower than that", or include something along the lines of a --polyfills compiler flag to allow you to opt into (or maybe opt out of) having polyfills included in the generated source.

The most important part to me is getting the best of both worlds: automatic currying when I'm writing Roy, and standard invocation style when I'm calling Roy functions from JS.

puffnfresh commented 11 years ago

CoffeeScript used to do this when you used its "fat arrow" (now it's smart enough to just alias this):

var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };

I think that's a good default. Roy should probably have an ES5 mode to allow you to not have that and just rely on a builtin Function.prototype.bind but that can come much later.

rtfeldman commented 11 years ago

Sure, that's definitely a minor optimization.

NickHeiner commented 11 years ago

:+1:

X1011 commented 10 years ago

@joneshf, I'm assuming by "plain JS developer" you mean a developer who writes plain JS in a functional style. ramda.js, @DrBoolean's libraries, and prelude.ls all take your approach and make the functions they export partially applicable, and they all call it currying. DrBoolean's implementation comes straight from wu.js, and prelude.ls takes advantage of LiveScript's built-in implementation. I've not seen any functional JS libraries that do honest-to-goodness, one-function-per-argument currying by default.

joneshf commented 10 years ago

I haven't looked at it too indepth, but underscore-contrib seems to actually do this: https://github.com/documentcloud/underscore-contrib/blob/master/underscore.function.arity.js#L127-L176 Based on https://github.com/eborden/js-curry But yeah, it seems like it's not the norm.

joneshf commented 10 years ago

Very nice @nickmeharry! Can't wait for constraints to land.