Higher order add an unused params?

Link: https://rescript-lang.org/try?code=C4TwDgpgBMULxQN4CgpQgDwgYwFxQAoBKeAPigEsA7YZAX2WQBsJZqLYEDsB7KgMwoBzEnHLEyUXgOHIA9HKgAZasAgAnKAHcAhjVzzFLNlQ7xC0wUIA0hIqPKXZxmBADOnKXyuTEmHPjswNzewkR0QA

why does the rendered js add an unused (and unecessary) param ?
And why my linter want my to write:
let init = (config, ()) => config
Instead of:
let init = (config) => () => config

I understand that it’s like the “curry version” by I’m not sure it make it more clear…

2 Likes

In a curried language every function only has one parameter. The code (config) => () => config is what really happens, but to avoid confusion (and unnecessary thunks when compiled to JS) combining all arguments into one is preferred which is why you see (config, ()) => config in JS and lint/format output.

To work around this uncurried functions exist. Add that extra dot and it will do what you expect (at the cost of changing how you call the function).

3 Likes

I have the same concern. If the objective is to output code that could live with hand made one it is very hard with unused parameters, which is a very common lint rule.

But why lint generated code?

2 Likes

That’s not the point he made, at least in my understanding.

If the aim is to generate code that looks as if it were hand-written, it makes sense to take common linting rules into account, because that would make the generated code more like it was hand-written, even when the generated code itself isn’t linted.

I don’t think that’s the aim, or at least not the only aim. It’s a benefit of ReScript’s design, but there are other tradeoffs to consider.

4 Likes

There are several reasons why this makes no sense.
The first, as @lensbart said, if the code is supposed to live along with other code that conforms to linting rules it should, at least, look like it. Having unused variable son JS is a common cause of problems and it is a bad practice that pops to your eye even without any linting active.

And second, if some of that code is expected to be used from JS side, having a function that takes an argument that is not needed does not help. Your editor/IDE will tell you that this function expects a param called param, of any type, and you will not know what to pass there.

2 Likes

Hi @danielo515

if the code is supposed to live along with other code that conforms to linting rules

The goal is that ReScript “compiles to efficient and human-readable JavaScript” (from the ReScript home page).

It’s probably helpful to set the right expectations since the JS code is generated. This comes with some pros and cons. The pros include optimizations in ways you might not write by hand, and the cons include code that is very readable but may not be exactly how you might write it by hand.

But your concern about the param argument is fair.

if some of that code is expected to be used from JS side, having a function that takes an argument that is not needed does not help

As @spyder mentioned, fortunately that can be resolved using uncurried function syntax. E.g.

let init = config => {
  (. ()) => config
}

Compiles to:

function init(config) {
  return function () {
    return config;
  };
}
1 Like

I totally missed that. I will give it a try, it looks like a reasonable drawback

Hi, I think this should be put in an FAQ section.

In ReScript, the curried function/application does not provide any guarantee in the JS interop level, only the uncurried function/application provides such guarantee.

The reason is performance. If you follow the naive model of curried calling convention, it would be:

let add = (x,y)=> x + y
// generated JS below using the simple model
function add (x) {
  function (y) {
     return x + y 
  }
}

Some languages like PureScript adopt this model, but this is too slow for real world usage. So we did lots of magic behind the scene to make it have good performance, which means the output of a curried function will be compiled into various forms depending on the optimizing compiler.

Having two calling convention is unfortunate, that’s the main pain ponit we want to tackle in the long term.

4 Likes

That is one of the things I liked the most about bucklescript, and I hope you keep it on rescript.

1 Like

Thank’s for this clarification, definetly need this written somewhere. What about a chapter about rescript optimization ? Still, it doesn’t explain the extra and unnecessary “param” argument that appears in the case of an higher function that return a function without params, but I guess it’s a micro-optimization level.

The unused param isn’t related to higher-order functions specifically. In ReScript, there is no such thing as a function with no params. () => ... defines a function that takes one parameter, () (unit type). It’s really just syntax sugar for (()) => ....

() is just a variant with a special syntax and defined roughly like this: type unit = | (). When ReScript has a function that accepts it as an argument, then it pattern-matches it like any other argument. Because there’s only one constructor for the unit type, and because it doesn’t contain any data, then ReScript just ignores it.

Another way of thinking about it is that these two functions are semantically the same:

let f1 = () => {
  Js.log("foo")
}

let f2 = param => {
  let () = param
  Js.log("foo")
}

You can even define your own “unit” type and do the same thing:

type t = Unit
let f3 = (Unit) => Js.log("foo")
let f4 = (#Unit) => Js.log("foo") // Polymorphic variant

All four of these functions compile to the same JS.

3 Likes

I see, shouldn’t js rendering not include any parameters in this case anyway ?
I guess we can deleted them with a compressor like terser and the correct option, but still.

1 Like

They’re absolutely optimised away by minification tools, which means doing the extra compiler checks to remove them is a micro-optimisation. I’m sure they would be happy to receive a PR but I don’t see it being a high priority.

1 Like