-
Notifications
You must be signed in to change notification settings - Fork 25
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
Discussion of Related Work (Similar features in Other Languages) #44
Comments
As discussed in #45 , Haskell and Elm for instance have taken a "simpler" path in the sense that partial application is applied naturally, meaning in the function arguments order. This means that if we want to call a function defined by subtract :: Int -> Int -> Int
subtract a b = a - b To create a new function called decrementOneBy = subtract 1 It can then later be used like decrementOneBy 2 -- -1 But the more real-world use-case for that would be to do the inverse, meaning decrement something by one for instance. There is a function called decrement = (flip subtract) 1 decrement 2 -- 1 But this means that the responsibility for doing that kind of work is passed to the caller. Haskell and Elm's functions are designed so that the data is always at the end whenever possible and the settings at the beginning so this Unfortunately, in JavaScript, there is some examples when the design used for the function signature does not allow a clean partial application and thus will probably require this special const powBy = (power, target) => target ** power;
const powBy4 = powBy(4);
console.log(powBy4(5));
console.log(powBy(4, 5)); This is where the proposal's operator is useful. const powBy4 = Math.pow(?, 4);
console.log(powBy4(5)); This works great, but I'm afraid the implementation behind this special syntax will be too much complexity added to the runtime interpreter and there must be some kind of similar reason why these languages do not allow this out of the box (only with the use of another function like Also, languages like Haskell and Elm have proven no real use of partially applying arguments backward, this is why I would highly be in favor of a syntax that only allows partial application from right to left. Even if that means writing down some of the already known helpers like A flip helper could be included in the ECMA specification for the already existing functions that needs to be flipped. Here is an hypothetical polyfill for those two helpers if it were to be added like that in the standard. Function.flip = function(callback) {
if (typeof callback !== "function") {
throw new Error("callback is not a function in Function.flip(callback)");
}
if (callback.length !== 2) {
throw new Error("callback does not take exactly two arguments in Function.flip(callback)");
}
return (a, b) => {
return callback(b, a);
};
};
Function.partial = function(callback, ...initialArguments) {
if (typeof callback !== "function") {
throw new Error("callback is not a function in Function.partial(callback)");
}
return (...additionalArguments) => {
const allArguments = [...initialArguments, ...additionalArguments];
if (allArguments.length >= callback.length) {
return callback(...allArguments);
}
return Function.partial(callback, ...allArguments);
};
};
const powBy = Function.partial(Function.flip(Math.pow));
const powBy4 = powBy(4);
console.log(powBy(4, 2)); // 16
console.log(powBy4(2)); // 16 It is worth noting that partially applied functions should not have any variadic parameters because it simply does not make sense to partially apply variadic functions. |
imo such a "flip" function couldn't ever be standardized, because JS functions aren't guaranteed to have exactly two arguments (nor any specific number) |
Flipping a function that has more than two arguments makes little to no sense in my opinion (hence why there is only a This would help writing less code for legacy functions that have this kind of argument design in the current standard but it is pretty easy to flip in any direction using a callback if the function were to have more than two arguments. const badlyDesignedReduce = (items, reducer, initialValue) => { /* TODO: implementation */ };
const fold = Function.partial((reducer, initialValue, items) => {
return badlyDesignedReduce(items, reducer, initialValue)
}); In this case, there is no need for the use of the The previous comment has been edited to add runtime type checking and a more obvious explanation of what the errors could be at runtime for the |
There are many, many JavaScript functions out there that take data first and optional argument or variable amount of arguments after that, and tons of people are merrily using them as we speak. Calling all these functions as "legacy" or "bad" just because they aren't designed in the same way that functions in Haskell or Elm are, sounds a bit... dogmatic. As far as I understand, the goal of this proposal is to narrow the gap between these existing functions and functional-style code, not make it wider. |
Interesting, could you elaborate more on what makes you think that this solution widens the gap between the existing functions? As far as I understand the goal of this article, it's interesting to compare the proposal to other programming languages and the way they address the kind of problem that this proposal is attempting to solve without the need for a new operator. There also has been some great solutions provided by the community here that does not involve the use of a new operator such as a new keyword behind function definition that makes it more obvious like in #40 I'm really curious to have your input on what makes you think that a function, in a functional-style code, instead of a "new" operator ( |
Let's say I want to call items.map(_ => JSON.stringify(_, null, 2)) This works fine, but the need to wrap my simple mapping function in a lambda introduces a small amount of With this proposal, I could instead write items.map(JSON.stringify(?, null, 2)) I find that slightly easier to read and more pleasant to write, although I also understand that beauty is in the eye of the beholder. This use case and the way to invoke these functions was perhaps not at the top of the usage examples when designing these functions, and that's OK because JavaScript is and is supposed to be a versatile multi-paradigm language that caters to lots of developers' widely different coding styles. With the help of the proposal, however, I believe even functions designed in this way - data first, variable number of optional arguments after - can be pleasant to use for developers who'd perhaps prefer a bit different kind of signature for them. I call this "narrowing the gap" because it makes the use of these existing functions more ergonomic in this particular manner and thus makes it more likely for developers of different backgrounds to use these existing functions without being compelled to create replacements or wrappers for them. On the other corner, we have currying and/or partial application functions that partially apply arguments from left to right. As discussed above, these solutions are kind of ineffective for functions designed with a signature like that of So, if you can't just items.map(Function.partial(Function.flip(JSON.stringify), 2, null) what can one do? Well, as you suggested with the reduce-example, they could create a new wrapper function that takes its formatting arguments in a different order, or no formatting arguments at all. I call this "widening the gap" because it discourages direct use of perfectly good existing functions in favor of writing one-liner wrappers. Some years back, when Promises were new and shiny and great but the builtin APIs in Node.js were not quite caught up yet, a common start for any of my Node script files was something like const fs = require('fs')
const readFile = (path, options) => new Promise((resolve, reject) => fs.readFile(path, options, (err, res) => {
if (err) reject(err)
else resolve(res)
}) Perhaps with Promises the paradigm shift from callbacks was big enough that this migration pain was warranted, but nonetheless, the pain was real. I don't long for writing these ugly little wrappers, not one bit. I find it unlikely that we'd start getting unary/data-last versions of these functions built in, so in this case there's no fs/promises that would come and save the day. Also as a sidenote, I find the that the readability of these alternatives goes (from most readable to the least) items.map(JSON.stringify(?, null, 2))
items.map(_ => JSON.stringify(_, null, 2))
items.map(Function.partial(Function.flip(JSON.stringify), 2, null) but I don't want to overemphasize that point since I know people will have different opinions about this.
Yes it is interesting, and I didn't mean to discourage good healthy discussion. But I just want to remind that JS is not the same language as Clojure or Haskell, and a notion that the aftermentioned languages are somehow objectively better or obvious directions where JS language design should be headed, is a very opinionated idea that is not shared by everyone. Calling
The actual sigil to use is up to discussion in #21. There's also a lot of discussion in other issues here about the benefits of an operator versus a function, like readability and runtime performance. An operator can also do things a function simply can't. Namely, it can bind the |
I'd like to chime in with LiveScript: https://livescript.net/#functions-partial They have it similar, as the proposal, that is The partialized functions are also autocurried, so if not every missing argument was passed, it returns another function awaiting remaining arguments: minus = (-)
oneMinus = minus(1)
oneMinusTwo = oneMinus(2) # -1
oneMinusTwo2 = minus(1, 2) # also -1 |
I would prefer this, however, since
Not sure if this is talked about in any other issue in this proposal, but I don't think, this is covered by the proposal yet, but would be a nice addition |
Clojure(Script) has a feature (docs) through which instead of writing:
you may write
Additionally, there can be a qualifying number after the mark to support multiple arguments (including rest arguments)
I think that a "Related Work" section would be a great addition to the README document and could benefit the overall discussion
As an example, looking at how clojure does it, we might resolve #5 and get an alternate design point for parsing. Clojure begins this syntactic sugar with a special token
#
which makes it easier on the parser. Does this proposal face the similar challenges in parsing? If so, does clojure's design help us alleviate it? These are some things which I believe should be a part of the discussion.I've discussed clojure, and it'd also be good to know how other languages implement similar features, and if we can take inspiration from their design / learn from challenges they ran into.
The text was updated successfully, but these errors were encountered: