Let keyword needed?

Why does ReScript need a let keyword, actually?

From what I can tell, it is not used to discern any of the bindings from one another:

If it is treated completely uniformly, does it bring anything? Why not remove it?

Some arguments in favor:

  1. Less is more. Code would be clearer. Less to read, and lines would begin with variable names instead of redundant keywords. Example: https://github.com/jihchi/rescript-react-realworld-example-app/blob/main/src/component/Link.res

  2. Other, (dynamic) languages (like Ruby, Python) don’t have biding keywords either. So it should be possible, syntax wise. An added benefit would also be that ReScript could garner appeal from people coming from such dynamic languages (like me :).

  3. let is easily confused by developers coming from JavaScript. Even the code examples used for branding on the homepage can immediately give off the wrong impression (“uck, mutability, some may think”). Since people from JS/TS have become accustomed to let as an antipattern. Even Reason has to explain that: *If you are coming from JavaScript, these bindings behave like const, not like var orlet*. What’s better than documentation? No need for documentation.

Some arguments against:

  1. Backwards compatibility. But would it be a breaking change? And if so, aren’t ReScript still in an early phase where it’s possible?
1 Like

I feel this is quite subjective.

For me let is an indicator that “This is where a new value starts and you can ignore this keyword before this line”

If I don’t see let, that usually means a value is used, so if I want to know where it comes from I know I have to search in another location until I find the let <name>. In that scenario it also makes a great search target if I want to know where a value is defined.

1 Like

let is a nice way to see where a binding is shadowed/redeclared. Consider the following code

let x = ref(0)
x.contents = 4
let x = 3

Without the let keyword here it wouldn’t be clear when the language was doing a mutable update and when it was shadowing the binding. This is especially important in a strongly typed language because shadowing may also change the type of the binding and so it’s important to easily spot where this is happening.


@Kingdutch wouldn’t the succeeding = sign give you that indication? Syntax highlighting also works to highlight variable declarations.

@tom-sherman yeah, but you couldn’t you achieve that even without the let ?

x = ref(0) // mutable reference, determiinned by the ref()
x.contents = 4 // mutable update, determined by the .contents
x = 3 // shadowing

It may be technically possible but my point was about clarity and semantics. Keywords aren’t evil, they are great markers that can aid readability.


I think this would be really confusing when using loops. As an example:

x = 0
while (x < 10) {
  x = x + 1;

Someone coming from a JavaScript background would expect this to print out numbers from 0 to 9, but it actually results in an infinite loop.

Here’s the same piece of code written using let:

let x = 0
while (x < 10) {
  let x = x + 1;

Seeing the let x = ... inside the while loop at least provides a clue as to what the issue might be.


@kevinbarash that’s a good point. Though since immutability is a core part of ReScript, it should ring a bell when trying to mutate that variable just like that.

Another thought is: why not rename let to const ? If the goal is to stay as close to JS as possible.

I think it’ll lead to more harm than benifits renaming let to const in all existing projects. Removing it completely or keeping both options is even more destructive.

1 Like

let is shorter than const, and IMO swapping may only lead to more confusion. Variables can be shadowed and the possibility of having two const x = ... in a block of code is likely to be more confusing to a JS/TS developer than any benefits gained by avoiding let. But that’s just my opinion.

As for why let at all… I believe it was originally because ML syntax also uses let (in a slightly different way, but one that ReScript syntax can convert to under the hood).


Ok. Thank you for all your insightful arguments. :slight_smile: You have convinced me to lay this issue to rest.

1 Like

I’m sorry, my curiosity is getting me… @spyder and @tom-sherman I wonder, is allowing variable shadowing a good idea in itself?

“The C# language breaks this tradition, allowing variable shadowing between an inner and an outer class, and between a method and its containing class, but not between an if-block and its containing method, or between case statements in a switch block.

On C# it is said that:

“the rules regarding local shadowing ensure that an identifier has exactly one meaning in a local scope” … “I have never written a bug where I accidentally shadowed a variable in another scope in C#.”

I imagine that simply not allowing variable shadowing within functions would be a good idea. It would also prevent the footgun @kevinbarabash mentioned.

Would such a restriction pose particular algorithmic challenges you can think of?

Shadowing comes down mostly to opinion, along the lines of “tabs vs spaces” and “static vs dynamic types”. I don’t think I’m smart enough to really comment on the algorithmic challenges of changing how variable shadowing works in ReScript. I’m sure plenty of compiler engineers have thought long and hard about it.

I think shadowing falls naturally out of the implementation of let in ML languages, where each let creates a new scope. That certainly seems to be how it happened in Rust (I found an interesting stack overflow discussion thread where this is mentioned, and further discussion on reddit).

Personally now that I’ve used ReScript/ReasonML/OCaml for nearly 8 years I’m quite comfortable with shadowing. The footgun @kevinbarabash mentioned specifically only applies when let is removed; with it I think the shadowing problems in the code are quite obvious.

I don’t shadow often, and when I do I try to make sure the shadow has a different type to avoid accidents, but I think it’s a useful language feature to have.


Shadowing directly results from the perspective that a lambda starts a new scope. Basically, conceptually think of let as desugaring to a new lambda:

let f = () => {
  let x = 1
  let x = x + 1
  x + 1

// ==>

let f = () => (x => (x => x + 1)(x + 1))(1)

If you have a language with a standard concept of a lambda, and the fact that lambda parameters can create new names, and that there shouldn’t be any artificial restrictions on the names like ‘can’t be an existing name’, then the same arguments apply to let-bindings. This was all thought out a long time ago when ML was standardized :slight_smile: