We are busy improving toolings this half so I don’t think we have budget for investigate adding optional support for uncurried calling convention
Ok, thanks. Maybe this can be revisited at a later point. I completely understand that tooling improvements have top priority for now.
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.
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.
Our mileage definitely varies
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.
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.
Perhaps autocurrying can be disabled just for res
, so all existing code keeps working? (Assuming optional arguments are addressed).
I’m also in favour of removing auto-currying, but I suggest the underscore for partial function application
f(_) f(_,a) ...
+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.
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.
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.
For me, uncurried by default would be a killer argument to switch our code from reason to rescript.
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.
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.
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.
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 in a curried language only have one argument
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.