Uncurried by default?

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

I agree too, having uncurried functions by default with a syntax for partial application looks like the best of both worlds.

7 Likes

Even though I frequently use partial application in my code, I’m in full support for uncurried by default. In my opinion, the benefits of more readable JS output outweigh the downsides of a potential new explicit syntax for partial application function declaration.

2 Likes

It’s not just more readable JS output, it would greatly simplify JS interop too.

6 Likes

I’m a fan of the auto currying and it’s one of my favorite features in ReScript. I would hate to lose that.

3 Likes

The main thing is that we have two calling conventions, it has a large mental overhead. And the API is designed like this: findByU (uncurry), findBy(curry).

There seems to be clear consensus towards uncurried by default.
Notice: the auto-uncurrying language will probably still exist, the same way as the uncurried language exists today (by using dots).

9 Likes