Uncurried by default?

Do we have numbers on that? My impression is certainly that it costs performance, and I’ve switched hot code to uncurried syntax, but I’m not totally convinced. Maybe it depends how hot the hot code is.

I’m on the fence with this suggestion. I can see the benefits, but uncurried functions are still weird in many ways and curried “just works”. I don’t think the perceived benefits of this change would be worth losing that.

6 Likes

I don’t see any benefit from removing one of the main features of the language. One of the reasons why I like functional languages is because automatic currying. I don’t think that you should remove a mayor selling point for few cases where you can just manually optimize it, which is in fact the only proper way of optimizing something.
If premature optimization is evil, this is the mother of all premature optimizations.
Also I don’t think you should underestimate the beginners that much. If they are trying rescript it is probably because they are interested by the language and features like this. Optimizing for the few people that may not like it is another premature optimization.
If you continue changing the language to match Javascript you will end with just a syntax on top of Javascript.

10 Likes

Placeholder arguments offer much more than currying: you can specify any argument position, and you don’t need any runtime on functions not using them.

let test = map(someNumbers, add(1, _))
let otherTest = map(someNumbers, subtract(_, 1))

It is also matches the syntax used for the pipe-first operator.

let number = someNumber->add(1, _)

And as a slight aside, if you wanted to expose currying (and I think we have to), I think we should pick a new syntax rather than repurpose the old syntax. Maybe this,

let add = @curry (a, b) => a + b
let test = map(someNumbers, add(1, ...))

@Hongbo out of interest, is the optional argument support impossible to add, or just needs the work to be done?

1 Like

@Hongbo out of interest, is the optional argument support impossible to add, or just needs the work to be done?

That would be my question, too. Losing optional parameters would be a dealbreaker for me. :slightly_frowning_face:

One of the reasons why I like functional languages is because automatic currying.

Having several big ReasonReact and Reason React Native apps in production, we found that we are only rarely making use of automatic currying in our code and would not mind if it needed to be made explicit in some way. YMMV, especially if you following a more “hardcore FP” style.

Still, I don’t think making currying explicit/“opt-in” would make ReScript less of a functional language than keeping it implicit/“opt-out”. It would just better fit the target platform (JS).

1 Like

I extensively use and appreciate currying across a large ReScript codebase. Losing it will be very sad. I’ve had performance issues in my ReScript codebases, but they were always due to the choice of bad data structures and algorithms. We’re now starting to work on a CPU-heavy codebase in ReScript, and I still don’t see any place where currying is going to be the bottleneck. If it becomes a problem, in the occasional case, we can always uncurry explicitly.

7 Likes

To clarify, I don’t think anyone is saying remove it. Only to make it opt-in rather than opt-out.

4 Likes

We are busy improving toolings this half so I don’t think we have budget for investigate adding optional support for uncurried calling convention

6 Likes

Ok, thanks. Maybe this can be revisited at a later point. I completely understand that tooling improvements have top priority for now.

1 Like

I think it really depends on the task at hand.

Most of our externals use explicit uncurrying because we encountered problems on several occasions, when we did otherwise.
Therefore I wouldn’t mind externals to be uncurried by default.

On the other hand we intentional use currying very often in our code base.

2 Likes

It’s possible with whole program optimization to intelligently uncurry every instance. The reason bsc isn’t doing it is because it cannot look further than the compilation unit. I think that it would be way nicer to have a “production” mode that automatically uncurries functions during compile. Cristiano also mentioned it could be added to reanalyze. To me this is the best solution because we can get rid of all somethingU functions in the standard library and we don’t have to manually uncurry functions on call and definition site anymore.

6 Likes

Our mileage definitely varies :wink:

Since we’re not talking about removing curry completely I think I would be fine with this, but not until uncurry has parity in as many ways as possible.

2 Likes

I think auto-currying has been on my top 3 list of most annoying part of ReScript. It has led to several runtime bugs for us (where a function gets curried instead of called, and it’s not catched at compiled time because the compiler can’t always tell that you didn’t mean to do that and no matter what we say about the compiler warning for effectful functions that should return unit, it still happens to us without any warning) as well as really hard to decipher error messages (because the compiler infers that let data = func(a, b) is a function, as it’s automatically curried, and will error out when you use data with a strange error. And if you’re unlucky, the error will surface much lower down the call stack, since the compiler will infer argument types etc…).

I wouldn’t want to lose optional arguments though, so if that’s the tradeoff I think we should keep it the way it is until we find a better solution.

5 Likes

Perhaps autocurrying can be disabled just for res, so all existing code keeps working? (Assuming optional arguments are addressed).

1 Like

I’m also in favour of removing auto-currying, but I suggest the underscore for partial function application

f(_) f(_,a) ...

1 Like

+1 for making currying opt-in if default/optional args is solvable problem. IMO flipping f(x) & f(. x) is fine. _ in this context has different purpose already so it wouldn’t work.

1 Like

I worry that re-using the . syntax would cause confusion for rescript newcomers who see . used in snippets in reason syntax (and vice-versa). perhaps * is an equally lightweight syntax?

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