Curried/uncurried single-argument functions

I’ve been struggling to acclimate myself to the new uncurried system. One of the things that puzzles me is what the difference is between curried and uncurried when there’s a single argument. For example, we extensively use @deriving(jsConverter) to make string-like enum types, and we have a helper function to turn those into decoders that will work with decco:

let makeStringLikeDecode = (name, fromJs, json) =>
  json
  ->Decco.stringFromJson
  ->Belt.Result.flatMap(s =>
    s->fromJs->ResultUtils.fromOption(makeDeccoError(`Invalid string value for ${name}`, json))
  )

And then it’s called like

@deriving(jsConverter)
type t = [ #First | #Second | #Third ];

let t_encode = t => t->tToJs->Js.Json.string
let t_decode = JsonUtils.makeStringLikeDecode("MyModule.t", tFromJs)

Because everything is uncurried now, I had to change the definition of makeStringLikeDecode to explicitly curry the first two arguments from the last:

let makeStringLikeDecode = (name, fromJs) => json =>
  json
  ->Decco.stringFromJson
  ->Belt.Result.flatMap(s =>
    s->fromJs->ResultUtils.fromOption(makeDeccoError(`Invalid string value for ${name}`, json))
  )

However, I’m still getting a compiler error now:

FAILED: src/MyModule-MyProject.cmj

  We've found a bug for you!
  /project/src/MyModule.res:5:63-69

  3 │
  4 │ let t_encode = m => m->tToJs->Js.Json.string
  5 │ let t_decode = JsonUtils.makeStringLikeDecode("MyModule.t", tFromJs)
  6 │
  7 │ 

  This function is a curried function where an uncurried function is expected

Can’t see it in the snippet above but the tFromJs is highlighted red here. This is strange because tFromJs only takes a single argument, so it would seem that there’s no difference whether or not it’s curried. Furthermore I’m not aware of anything I can do at the code level to change it. Inside of makeStringLikeDecode there’s just a function call to fromJs with one argument. I tried changing s->fromJs to fromJs(. s) but that had no effect (I’d seen discussed that the dot syntax might be used to indicate a curried function call, but it would appear that’s not a thing now). So, how can I tell the compiler that fromJs is supposed to be (or is allowed to be) uncurried?

Beyond that, I don’t think @deriving(jsConverter) should be emitting curried functions in the first place, if the compiler is in uncurried mode. If there does need to be for some reason a distinction in the compiler between curried and uncurried for single-argument functions, it would seem that the auto-generated functions should be uncurried.

Workaround:

In the process of writing this post I discovered a workaround, which is to change the call site:

let t_decode = JsonUtils.makeStringLikeDecode("MyModule.t", js => tFromJs(js))

This I suppose will unblock me moving forward, but it’s quite cumbersome and redundant to add in the extra lambda function there. And having to go around updating several dozen places in our code base with this change is less than ideal. It should be possible to fix within the function itself so it “just works” rather than needing to update a bunch of call sites.

1 Like

Building on this, I also discovered that the curried/uncurried single-argument functions can cause extremely obscure errors from @decco:

  We've found a bug for you!
  /project/src/MyModule.res

  This function is a curried function where an uncurried function is expected

With no line numbers or anything, this was super hard to track down, but eventually I figured out it was being caused by the t_encode and t_decode from another module which were declared as

let t_encode = Js.Json.string
let t_decode = Decco.stringFromJson

and in order to fix them I had to rewrite them to

let t_encode = j => Js.Json.string(j)
let t_decode = j => Decco.stringFromJson(j)

Which is baffling because I can’t for the life of me think of a single difference between these two implementations; aside from the redundancy of an extra lambda function.

Now I’m getting strange errors from modules which use Apollo ppx, which is making me nervous because if I have to update the ppx in order to get things working, that’ll be interesting…

If you’ll forgive me venting a little bit it’s just really frustrating that the community seems to have given so little thought to the downstream effects of removing currying from the language.

I read the entire thread discussing the justifications for the change and the great majority of the reasons seemed to involve external bindings. Well, personally for my team it’s extremely rare that we run into any problems with correct bindings because of currying and they are easily addressed when encountered. But challenges with external bindings could be solved simply by extending or modifying the binding system and syntax as needed, which seemed to hardly be discussed as a consensus on changing to uncurried-by-default was reached (or perhaps, was declared. I saw plenty of people objecting to it in that thread but they seem to have been ignored).

Another concern was the difficulty of understanding currying. In my opinion this is both overstated and unimportant. All languages have a learning curve of some kind and as far as languages go I think rescript is quite approachable. I don’t think currying significantly complicates it, and though I had a haskell background before rescript, no one on my team has had trouble understanding currying even if they don’t come from an FP background.

Some other concerns were about overhead of Curry.X calls, or the need to put a () as the last parameter when using optional named parameters. But no one provided any numbers to justify that the overhead of currying created any performance hit, and others in the thread suggested that according to their testing it didn’t. The need for a terminal () is completely inconsequential in my opinion. It’s just something you do it and everything works just fine.

Then there’s the fact itself that we’re losing the currying feature and the expressiveness that comes with it. Almost uniformly, updating our code to be uncurried-compatible has resulted in less readable, more verbose, and/or redundant code. It’s extremely frustrating to not only have to spend hours updating function calls throughout or codebase, but to be replacing neat and expressive code with a garbled mess of nested function calls and single-argument lambdas in the process. I used to be able to write json |> field("myField", array(string)), now I have to write field("myField", array(string))(json). I used to be able to write Js.Array.map(config->getValue), now I have to write arr => arr->Js.Array2.map(v => config->getValue(v)). Is it minor? Perhaps. But it’s uglier, and it was forced on me.

Based on justifications which I really don’t find convincing, the rescript team decided to make a massive change to the language semantics which broke dozens of packages and thousands of modules. Our company has over 200k lines of rescript code, we’re heavily invested in the language, and this change has made the effort required to upgrade to the latest version, rewriting thousands of lines not just in our app but in many dependencies, all to solve a so-called problem that for us never was a problem, on the contrary it was a valuable feature.

I realize the train has left the station at this point but after all the time I’ve spent on this upgrade, and with an unknown amount of work left to go, I have to say something.

1 Like

I thought you could get the old behavior by setting

{
  "uncurried": false
}

in rescript.json. Or am I misunderstanding the problem?

@kay-tee When I set that in my project it sets it as well for every dependency in the project. So the libraries which have been updated to work in uncurried mode, no longer work. Unless I’m missing something, it’s all-or-nothing.

If you disable auto currying that also applies to dependencies, and it won’t work with the new ReScript packages like core.

Currying is still part of the language, but you have to opt each function into it. It can be a pain if you have to migrate a large code base, but old versions are still around and should continue to function.

Single arity uncurried functions can be passed directly as an argument into another function. I think the weird error messages are because in ReScript 11 most of the standard library is still curried even if you set uncurried to true, and that causes some really weird error messages. I also don’t think it’s great that it behaves like this it is at least very confusing. That is also why we didn’t convert our 500kloc codebase to uncurried yet (we are at the latest ReScript 11 with uncurried set to false).

Also note that ppxs could still emit uncurried functions, that could cause these weird errors. (For instance decco for sure is not updated to support uncurried mode yet).

I think it makes sense to wait for rescript 12 where the standard library is 100% uncurried to make the transition to uncurried.

1 Like

If you migrate to 12 it would mean everything is always uncurried by default, which would break some PPXs. You would need to wait for those PPXs to be updated, or switch to a other solution.

I’ll echo what @jfrolich says - probably a good idea to go to v11.1.x but stay on curried, and then move to v12 and uncurried when v12 has shipped as stable and the relevant parts of the ecosystem has adapted to it. Most of the widely used packages are already uncurried ready, so going to v12 is just a matter of dropping the dependency on @rescript/core since that’s now integrated into the compiler. But there are a few exceptions of course, where the migration to uncurried hasn’t happened yet.

1 Like

@adnelson are there any external dependencies you have that haven’t migrated? I’d be happy to help out with updating anything that’s open source.