Every once in a while Google For Developers pops up on my Instagram feed. They often present small programming-related challenges. This one jumped out to me in particular:
The video focusses on this block of code*:
for (var i = 0; i < 5; i++) {
const button = document.createElement('button');
button.innerText = `Button ${i}`
button.onclick = function () {
alert(`you clicked button ${i}`)
};
document.body.appendChild(button)
}
As shown in the videos, the buttons have the right numbering, but the alerts all show "You clicked button 5"!
The solution to the problem is very simple: replace the var
binding with a let
binding. Sentry has a great post comparing var
and let
, but the gist of it is that a let binding creates a variable in a smaller scope. This means that every iteration of this loop has its own instance of i
; with var
the same i
is shared between iterations, leading to i
being 5 for all onClick handlers by the time the loop is done.
Our saviour: Immutability
Let's see what the same code looks like in F#, when using minimal libraries on top of Fable. You can use the Fable online repl to run the code yourself.
open Browser
for i in [ 0..4 ] do
let button = document.createElement "button"
button.textContent <- $"Button {i}"
button.onclick <- (fun _ -> window.alert ($"You clicked button {i}"))
document.body.appendChild button |> ignore
No weird behavior to see here! The root cause of this problem lies in mutability, or the ability to change a value after it has been set. Because the i
value in the JavaScript example can change, it's very well possible that once it's used the value could be different, and as you can see in this example, that is not always obvious.
Fortunately, F# values are immutable by default. Unless you are deliberately adding the mutable
keyword when binding a value, you'll know it will stay the same forever.
The syntax of F# also makes sure that it is very obvious when you are changing mutable values; you can see that when we assign values to the attributes of the button. Instead of the regular =
operator, we use <-
, which makes it very explicit that we are changing a mutable field.
For those who really want to see the world burn, here's an F# example using explicit mutability to achieve the same result as the original JavaScript:
open Browser
let mutable i = 0
while i < 5 do
let button = document.createElement "button"
button.textContent <- $"Button {i}"
button.onclick <- (fun _ -> window.alert ($"You clicked button {i}"))
document.body.appendChild button |> ignore
i <- i + 1
Losing the remaining mutability
Fable.Browser, the library that we use to interact with the browser in the snippets above, is just a very small wrapper around JavaScript, and therefore still relies on mutability. In a full-scale project, we tend to reach for Elmish using a DSL like Feliz, allowing us write things like this:
for i in [ 0..4 ] do
button [
prop.text $"Button {i}"
prop.onClick (fun _ -> window.alert ($"You clicked button {i}"))
]
.NET is vulnerable too
Important to note is that it's not just JavaScript that is sensitive to this particular issue: C# for loops also reuse the same mutable variables for every iteration of a for loop, and will therefore display the same behavior. Since we've now officially left the realm of client-side web dev, a slightly more fabricated example:
using System;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
List<Func<string,string>> functions = new List<Func<string,string>>();
for (int i = 0; i < 5; i++){
functions.Add(x => x + i.ToString());
}
foreach (Func<string,string> function in functions){
Console.WriteLine(function("Hello!: "));
}
}
}
Prints:
Hello! 5
Hello! 5
Hello! 5
Hello! 5
Hello! 5
Conclusion
As Prash said: "This is weird, scary and upsetting". While F# is not the most pure functional language that's out there, it helps us steer clear of unexpected side effects like the one shown here.
* notice the slight deviation from the code in the reel: they posted they fixed version instead of the version with the bug