-
-
Notifications
You must be signed in to change notification settings - Fork 95
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
curry2 :: ((a, b) -> c) -> a -> b -> c #277
Comments
+1 on currying, generally. My 2 cents: Yes, the type signatures of the Ramda functions aren't very descriptive, but for me that doesn't outweigh the benefits of making them available. |
Thanks for the feedback, @EvanBurchard. In my experience blindly currying all the functions provided by a foreign module is not the best approach. It's likely that one of the following will apply to at least one of the functions:
I'm quite comfortable with this level of verbosity given the benefits it affords me: const S = require('sanctuary');
const _ = require('some-foreign-module');
// foo :: Array String -> String -> String
exports.foo = S.curry2((faulates, molake) => _.foo(molake, faulates));
// bar :: Number -> Number -> Number
exports.bar = S.curry2(_.bar); |
The big issue for me is not so much the behaviour of Ramda's curry function, but the behaviour of the functions it produces. I find them nearly impossible to work with. I would much rather work with an uncurried function and lose some pointfree, than a function curried Ramda-style. I prefer Haskell-style currying to either, of course, but some find the look of On the signature of One thing worth noting, is that our signatures for all polyadic functions are wrong.
We could choose to define our own argument-tuple type Then we could have
Not suggesting this is a good idea, but it is possible. |
I'd love to know more about your issues.
Noted. I'm going to take quite a bit of convincing, particularly since we took the "unprincipled" path with
Do we? The signature of
|
If I misapply a Ramda-style function so it receives too many arguments (normally as a result of a higher order function (especially array methods, but also well formed higher order functions)), I get very unexpected behaviour.
Fair enough. I do quite a lot of the lambda wrapping mentioned in that issue nowadays, so perhaps I have a slightly different opinion on what's painful to others.
Oops. My fault for assuming. However, that still means we're free to use whatever semantics we desire for argument tuples, as they're not used anywhere else |
Ah, I see! This would not be a problem with Does this ease your concern? |
A fair bit! It's been a while since I've had a chance to use Sanctuary. I forgot how wonderful it is[1]. In the (somewhat unlikely case) that I accidentally apply a function to additional arguments of the correct type (perhaps variable types), I'm not sure whether that will be able to help? [1]: I'm actually doing a talk on error-handling soon largely informed by Sanctuary's helpfulness |
The first thing sanctuary-def does is check const $ = require('sanctuary-def');
const def = $.create({checkTypes: true, env: $.env});
// f :: Any -> Any -> Any
const f = def('f', {}, [$.Any, $.Any, $.Any], (x, y) => x + y);
f.length;
// => 2
f(1).length;
// => 1
f(1, 2);
// => 3
f(1, 2, 3);
// ! TypeError: ‘f’ requires two arguments; received three arguments
Excellent! Please post your slides in Gitter after the talk (if you make slides). |
Is there an issue with a general Especially with I'm not super opinionated on this, as lately I've been just manually currying by determining how many parameters I will want to initially pass in, and then returning a function that takes the remaining parameters. I would definitely want to support
This is really quite exciting! I've been experimenting with how far I can go with just Sanctuary and not Ramda. Currying is definitely one of the main sticking points I've hit, so this is great. |
Something I want to point out here is if it is a goal to have "sanctuary as replacement for lodash/ramda," there's a bit more momentum for a general https://github.com/getify/Functional-Light-JS/blob/master/ch3.md That doesn't make them right, but it suggests to me that people might think of it as an all-or-nothing thing that is not dependent on arity. Right now, I feel like the path to learning FP in JS involves (along with reading those resources/watching talks):
I'm just wondering what would provide the least friction between going directly from bullet point 1, 2, or 3 to 4. HTML5 game frameworks had this similar thing, where people seemed to like either just using something that provided simple APIs or heading straight for Unity. There were a lot of projects that seemed to die on the hill of requiring a build process. This is not that bad. Just something I consider with various libraries. |
Let me tweak that example slightly to show my issue. const $ = require('sanctuary-def')
const S = require('sanctuary')
const R = require('ramda')
const def = $.create({ checkTypes: true, env: $.env })
const [ a, b, c ] = S.map($.TypeVariable, [ 'a', 'b', 'c' ])
const f = def('f', {}, [ a, b, c, [a, b, c] /* perhaps wrong syntax */ ], (x, y, z) => [ x, y, z ])
R.map(g => g(3), R.ap([ 'a', 'b', 'c' ].map(f), [ 1, 2, 3 ])) Here we'll get an |
Well-defined output types are just as important to me as well-defined input types. One could argue that input-dependent output types are idiomatic in a language as dynamic as JavaScript, and there are many libraries—such as Ramda—which cater to those who agree. I have a different viewpoint. There is no compiler to scour our programs for errors, so the onus is on us as programmers to reason about the types of the values flowing through our programs. This makes it even more important to use simple functions which are explicit about arity (at the very least). It's clear that Another concern is implementation complexity. The Ramda team has shown a willingness to accept complex implementations in order to provide the desired API. With complexity come edge cases. With edge cases come bugs. Again, because we have no support from a compiler it's important that we rely on functions we can reason about. There's a striking difference in complexity between the implementations of
That's enough for me. Let's provide
I've done this dance many times before. I appreciate the fact that Ramda-style currying saves me from having to choose one of the following types (for some quaternary function):
Although Ramda-style currying is at odds with my preference for well-defined types (as a function can have all the above types at once), I like to view |
Good point, @EvanBurchard. "FP in JS" is just part of the functional programming journey, though. Ideally people will go from Underscore to Ramda to Sanctuary to PureScript to Haskell to Idris and beyond (while pursuing other wonderful approaches such as Lisp). 😄 The more concessions we make in order for Sanctuary to seem familiar to Underscore and Ramda users, the less familiar PureScript and Haskell will seem to Sanctuary users. One day I'd love to hear a story about a team that had used Ramda, adopted Sanctuary for increased type safety, used it successfully for some time, then realized they could write the same sort of code in PureScript and thus replace their run-time type checking with compile-time type checking. |
Thanks for the example, @rjmk. I see the problem now. If a function takes three arguments of any types and we accidentally provide three arguments when we meant to provide only one, there's no way for the function to know this was a mistake.
I actually prefer this to having the program work, as it would with manual currying. If I provide too many arguments to a function I'd like my program to crash (with a helpful error message, ideally). |
@davidchambers thanks for addressing my points. One nitpick I have is the "saving one keystroke" argument though. It's not just one keystroke for the cases I mentioned. Changing arity of a function involves changing call site, definition, and currying-site. Also currying multiple things (in an array/object) at once is not as simple to map over. Maybe I'm not embracing the right spirit here, but I my expectation most of the time is zero, one, or infinity. Maybe that's just a feeling I have right now that will change, but imagining a conversation with a coworker like "can we curry six? What just 5? uh. ok" kind of makes me twitch in the same way that the On the other hand, I've softened my position on long/descriptive variable names (eg. for type vars), although I still look sideways at tl:dr; I don't know man. I usually think in (my) ideal interfaces rather than ideal types, but I'm new to this game. You do you. |
I can see that. However, the error is much too far from the source of the problem (here it all occurs in the one-liner, but wouldn't have to in actuality). If things were strictly curried, Sanctuary could be catching these problems at the source. |
That's true. Wouldn't updating the call sites take most of the effort, though? Say we are wrapping a binary function
I like the ZOI rule very much. Arbitrary limits are unfortunate, I agree, but are required in some cases. Scala functions cannot have more than 22 parameters, for example. > R.curryN(11, R.identity)
! Error: First argument to _arity must be a non-negative integer no greater than ten Note: It is possible to avoid this limit if one is willing to use the |
Excellent point, @rjmk! This is an argument in favour of regular currying over Ramda-style currying. |
Interesting stuff on the limits with scala/ramda. And you're right that N call-sites still means 1 update to the curry2/3/whatever, not another N. As far as interface, were you picturing a flexible type that still allows the uncurried version? I'm guessing you're against that:
|
Yes, that's what I have in mind. Here's the // curry2 :: ((a, b) -> c) -> a -> b -> c
var curry2 =
def('curry2',
{},
[$.Function([a, b, c]), a, b, c],
function(f, x, y) { return f(x, y); }); Note that Usage example: // pow :: Number -> Number -> Number
var pow = curry2(Math.pow); Each of the following expressions evaluates to pow(10)(3);
pow(10, 3);
pow(S.__, 3)(10); |
Now that Sanctuary is close to being an alternative to Ramda rather than a library intended to be used alongside it, we should consider which Ramda functions we would miss were we to remove Ramda from our projects.
R.curry
andR.curryN
are certainly important functions.def
performs currying, of course, but not every project which depends on Sanctuary also depends on sanctuary-def.The type signatures of these Ramda functions are misleading:
Accurate type signatures are an important tool for reasoning about functions. The Sanctuary functions would be much simpler:
We must choose the maximum supported arity. I don't think it's necessary to go all the way to
curry5
;curry2
andcurry3
should suffice. If you call recall usingR.curry
orR.curryN
to produce a function of arity greater than 3, please speak up.🍛
The text was updated successfully, but these errors were encountered: