Uncurried by default?

Wouldn’t the newcomers confuse * with JS generators? I’d expect newcomers from JS to be more numerous (and more easily confused on average) than newcomers from Reason.

that’s a fair point…

Regarding the performance implications of runtime currying… I’ve done quite a bit of performance testing in this area, and I can tell you that it’s rarely an issue. The difference can seem huge when you look at micro-benchmarks, but we are talking about tens (even hundreds) of millions of calls per second.

Function call overhead can add up if you’re running lots of nested function calls over huge data sets, but performance issues are far more likely to come from the use of O(n^2) algorithms, or long-running processes generating tons of garbage for the GC to collect. Either way, it’s usually not about function call overhead.

I hate to be “that guy” who tells you to profile your programs and find the real performance bottlenecks before jumping the gun on auto-currying, but it’s honestly the right advice.

Auto-currying is an immensely valuable feature of the language. That’s why most serious FP libraries in JS-land implement a curry function. It’s just so useful. Partial application is one of the best ways to “pre-configure” your callback functions, for example. It’s the FP version of dependency injection, and it’s such a powerful tool. Auto-currying makes this technique a lot easier to read & to write. I would hate to see that feature go.

And one final thought. If curried function performance is truly a major concern, I think the better solution is to use third-party tools to optimize the JS output and inline those function calls. Several tools already exist for this purpose, although results may vary.

20 Likes

Just to be clear, we have been discussing the uncurried-by-default topic because it really is a pain for many of our users to troubleshoot buggy behavior that was caused by unintentional currying. Just as @bsansouci said:

There are a lot of annoying traits that come with auto-currying. My personal nitpick is the final unit argument for functions that use optional labeled arguments (let myFun = (~name=?, ())). It’s hard to explain, and it doesn’t make any sense at all.

Also, you get into really weird type signature situations whenever you try to write externals to JS functions that simulate currying like this:

const myFunc = (context) => (moreContext) => {...}

I ask you to write bindings for this particular case. The problem is that ironically we can’t reflect that properly, because our functions are automatically curried, so:

// some_binding.resi
type context
type moreContext
let myFunc: context => moreContext => (unit => unit)

will be reformatted to:

let myFunc: (context, moreContext, unit) => unit

Yes, there are tricks to make the compiler understand our actual intent by introducing an intermediate type, but IMO this is mostly a hack:

type context
type moreContext

type retFn = unit => unit
type moreContextFn = moreContext => retFn

let add: context => moreContextFn

And again, we want to be able to sufficiently map to JS, and higher order functions are a common pattern. Auto-currying makes this harder for us.

Another issue is callback functions. Callback functions are pretty much everywhere, and theoretically, if we want to map to clean JS, we need to uncurry every single callback function that is passed to an external JS function:

external myCoolFn: ((.string) => unit) => unit

For these cases we need to educate our users that they need to explicitly uncurry those, otherwise they pull in the Curry runtime module (less idiomatic JS output, more infrastructural costs, and I am not even arguing about performance here).

Really unfortunate we have to explain the theory of currying / partial application just to make ppl emit the right JS code.


IF uncurried-by-default would actually be a thing, we wouldn’t remove curry functionality completely. It would just be an opt-in syntax, just like the (.) => {} is an uncurried function right now.

I know where you are coming from. You have a FP background and you want to leverage all the FP patterns… but we are not necessarily advertising ReScript for its FP features, but more as a language to build type safe applications that compile to clean JS (with almost instant compilation times). I’d argue that we don’t care as much about subjectively clean looking code, but more about building solid products without much hassle or scientific type wrangling.

I guess we have enough users from the trenches that got beaten up by weird currying behavior so that it’s definitely worth to have these discussions. Not sure if it’s even technically feasible to make functions unurried by default, so everything we discuss here is pretty theoretical.

10 Likes

For me, uncurried by default would be a killer argument to switch our code from reason to rescript.

2 Likes

My personal nitpick is the final unit argument for functions that use optional labeled arguments (let myFun = (~name=?, ()) ). It’s hard to explain, and it doesn’t make any sense at all.

I find this annoying, too. It’s not ideal, but it does make sense after you understand the underlying issues. That doesn’t make it any better, but it’s an important distinction. The final unit argument is there for a reason.

And again, we want to be able to sufficiently map to JS, and higher order functions are a common pattern. Auto-currying makes this harder for us …
… Callback functions are pretty much everywhere, and theoretically, if we want to map to clean JS, we need to uncurry every single callback function that is passed to an external JS function.

Yeah, this is probably the only non-superficial thing that frustrates me. It can cause runtime bugs, and I’ve seen it happen. I’ve written tons of bindings (including most of the Node.js bindings), and this is a real issue. I’d be happy to discuss ideas for solutions to this.

IF uncurried-by-default would actually be a thing, we wouldn’t remove curry functionality completely. It would just be an opt-in syntax, just like the (.) => {} is an uncurried function right now.

I actually don’t mind the idea of a special syntax to curry functions. But there is one caveat to that… I don’t want the function definition to dictate whether a function can or cannot be curried. I want to be able to curry any function at all.

This is really important to me. The current situation with curried/uncurried syntax treats them as totally incompatible types. If I’m expected to pass a “normal” function, then I cannot pass an explicitly uncurried function, and vice versa. If “uncurried-by-default” is implemented in the future, I would like to have these types unified, so that I can receive an uncurried function, curry it if I want to, and the pass it to a HOF that expects an uncurried callback. Otherwise, what’s the point?

I know where you are coming from. You have a FP background and you want to leverage all the FP patterns…

It may seem that way from the way I talk, but I actually come from a highly imperative/OOP background. Perhaps I have the “zeal of a convert” when it comes to FP, but just to be clear, my prior programming experience was with assembly, C++, Visual BASIC, JavaScript & TypeScript. I had to learn these ideas the “hard” way, and the community support was invaluable.

I don’t think currying has to be hard to learn. It’s a bit different, but you would be surprised how easy it is to pick up when you have a clear explanation and concrete examples. Just having a dedicated documentation page on currying would go a long way.

But yeah, the external bindings situation is a genuine problem, and it’s worth solving IMO.

9 Likes

Honestly even I am not entirely sure if I understand the whole curry / uncurry situation in its fullest, and I also don’t know how to efficiently teach the rationale behind all of it. In my current workshop material, I tell folks that the currying feature exists, that this is an inherited attribute from its original FP roots and that they should stay away from currying until they feel comfortable with the rest of the language. It might sound weird, but from my experience I could practically spend a whole noon just teaching and practising all curried / uncurried / labeled / optionally labeled / … function constructs and ppl would probably have less confidence in using the language than before the course.

I also never found a good practical use-case for partially applied (optional) labeled arguments, because I always feared that it will bite me later, since I don’t know how and when previously applied labels will be erased. For some reason it messes with my mind, and I’d rather just write a new function closure myself than trying to memorize the rulesets (just like in JS). Fortunately this also aligns with our idiomatic JS output approach.

This is highly subjective of course, so YMMV.

Sorry, I didn’t want to make a judgement about your knowledge, I saw that you have a lot of experience with many different paradigms, and I appreciate that you are teaching others new concepts and new ways of thinking. I originally came to the “curry all the things” mindest when I was introduced to ramdajs, flow, fp-ts, drboolean’s functional course and when I learned about the fantasy land spec etc.

My personal conclusion after this experience (community, libraries, specs, PLs, working on projects) is that this particular part of the JS community will probably stay niche because of many different (social) issues. I saw myself, and many other FP-JS enthusiasts, ending up in a pretty weird echo-chamber, where we were all repeating back our positive opinions about category theory, monads, functional composition, pureness etc, while in the meantime, we completely ignored the rest of the JS community and all the folks that didn’t understand our (abstract thinking) mindset.

(Probably a bad argument, not sure, but I also always wonder why “newer” programming languages like Swift, Go, Rust, Kotlin don’t use auto-currying as well. Maybe because it’s easy to replicate currying in user-space? Same as with t-first API design… all of them go with the same approach, and we will probably need to ask ourselves why)

Anyways, now with ReScript we have a real chance on taking the practical parts of all that FP jazz, remove the fancy words, polish the syntax in a more JS friendly manner, and reduce the number of concepts to learn so ppl can focus on the IMO “more relevant” parts of the language.

Yes, I agree, that’s a huge problem (and the ReasonReactNative folks complain about that for uncurried functions as well). I guess the only way to circumvent type mismatches would be to implement a curry function in userspace (just like in JS)?

It’s a really complex topic and both opinions are valid, depending on how you look at it. Either from an API definition perspective (what is more common: curried or uncurried?), or from a community mindset perspective (functional programmers, imperative / OOP programmers) etc.

No matter what the decision might be, I would probably be happy with it, since curry / uncurry is “just a very small feature” (for me), and I was able to work around most compatibility issues so far. Would be glad if we could skip the whole teaching part though.

2 Likes

I agree with the idea that currying should be an explicit opt-in each time it’s done. In ReScript, would it make sense to require users to use the _ placeholder to manually curry a function? Or perhaps emit a warning when a function isn’t fully applied, which could be silenced by using a _ placeholder?

let f = (a, b) => a + b
let c = f(1) // Triggers a warning/error
let d = f(1, _) // No warning

It wouldn’t solve the underlying complications of having curried and uncurried functions be two separate types, but it would help alleviate the frustration where users accidentally forget an argument and cause runtime errors.

5 Likes

Yeah, it’s like Object.assign actually: very indirect and you never know what you’ll get. And like object configs in JS, they’re probably better being set in one place.

Maybe it’d be nice to provide all the optional parameters at once to parametrize the function, and still be able to use it later, but I guess designing the boundaries on that would be pretty complicated.

That, I’m sorry to say, is because they are completely incompatible. A ton of effort has been put into nice compiler errors around uncurried functions but in the early days of BuckleScript the truth was obvious. As far as I recall, an “uncurried function” isn’t actually a function as the compiler understands it. It’s a hack that wraps everything up in a value the emitter is able to use to produce nice JS. A function can never be used as an uncurried value because OCaml (and in fact all ML languages I know of) does not differentiate between a function call that executes code and one that does nothing, “auto currying” as some call it, producing only another function.

There is a fundamental problem that perhaps explains all of this chatter about functions, which as an experienced functional programmer is making very little sense to me. It’s exemplified in this comment:

I have always found the ReasonML function syntax to be misleading in this respect because it hides the truth. And you cannot ignore the truth no matter how hard you try. Repeat after me:

Functions in a curried language only have one argument.

Functions in a curried language only have one argument

Functions :clap: in :clap: a :clap: curried :clap: language :clap: only :clap: have :clap: one :clap: argument :clap:

I realise I’m retreading FP 101 but this is a very important point that the conversation seems to be ignoring. The implementation of myFunc, for example let myFunc = (c, m, u) => (), is just a shortcut. What is actually defined is this:

let myFunc = (c) => {
  (m) => {
    (u) => {
      ()
    }
  }
}

You can’t just pick and choose the things you like about OCaml. Curried functions are a fundamental concept of the language and while I can appreciate it is confusing for newcomers, trying to sell ReScript to JS/TS programmers by pretending it doesn’t exist will only leave them stranded when they run into it.

Dealing with accidental currying, and learning how to structure your code so that it leads to a type error instead of a runtime error, is just part of the contract you signed when you started using the language. I don’t think it should be buried in the fine print.

However I am probably on board with swapping curried and uncurried. Even if they aren’t equivalent and I have to use a manually curried function to leverage optional labels. I already have a mishmash of () and (.) sprinkled more or less randomly in my codebase, and when I eventually switch to ReScript it’ll be littered with list{} as well. It’ll just be one more weird thing we have to put up with to write code the way we want to.

19 Likes

For what its worth, I’d take the tradeoff of having slightly less readable JS in this regard than not have currying on by default. If I want the zero cost abstraction I’ll just use the Js.* API.

Thanks!

7 Likes

This is experimental, but it is so exciting that I would like to share: it is possible to support optional argument with uncurried too, so uncurried is as expressive as curried, note to land such feature would take some time.

Make the transition is a big change, we will find ways to do such transitions incrementally.

The major motivation of favoring uncurry instead of currying is not performance, it is to save the mental overhead, the semantic mismatch between ReScript and JS caused a huge headache and tons of bugs.

13 Likes

I don’t know how far you are with the v10, but maybe this is a good candidate for the experimental v11.

Personally, I can’t wait to play with it, but it might be a bit much for current production projects.

this is unlikely to land in v10, hopefully with an experimental flag

1 Like

Thought this would fit perfectly in this discussion, so I want to post it here:

Note that currying, especially with Ramda, has a very large negative performance impact.

https://news.ycombinator.com/item?id=28583319

5 Likes

The example used to demonstrate this “very large” impact in the article was a totally synthetic benchmark (a + b). I also don’t think Rambda’s dynamic curry system is really comparable to how curried functions work in ReScript.

I am on board with changing the default - most of the time it’s what I want, and losing the Curry._1 calls will simplify debugging - but I don’t see currying as having any performance impact in my app.

9 Likes

Not sure if this was a candid statement, but still an interesting tweet:

Yaron Minsky: “Hot take: currying is bad.”

2 Likes

I’m for uncurried by default. I was against it, because it makes the language less functional in the common sense. But! The convention of data-first arg almost made the whole idea of particial application questionable (killed it) in ReScript.

I don’t mind using _ to explicitly state a particial application (currying emulation) in rare cases when it is necessary. In exchange we can get better error messages, get them earlier, and make the JS interop more straightforward.

2 Likes

The thread adds interesting nuance to the hot take:

I mean, it’s cute and all that you can define multi-argument functions in terms of single-argument functions. But you can also encode tuples and integers as functions, and no one does that.

Partial application is good, of course. But the partial application afforded by currying is annoying, since you have to do the partial application in order. It would be better to have a lightweight syntax for partial application.

Currying also makes things like ownership disciplines unnecessarily complicated, and requires a bunch of cleverness to do efficiently. And it leads to worse error messages, reduces the power of the type system to catch bugs, and produces a more confusing syntax for types.

Of course, it should be possible to define curried functions. But there should be a first-class notion of multi-argument functions, and that should be the default

+1 from me!

I don’t mind using _ to explicitly state a particial application

Of course one would need a dedicated syntax to opt into partial application, but this is a good point: a bunch of cases of opting in could be handled gracefully by using _.

1 Like