United Kingdom: +44 (0)208 088 8978

Google hates this one weird trick for having NO bugs!

Ever have weird side-effects in your code? No, because you use F#? Good! Today we look at what can go wrong with mutability in JavaScript, C# and evil F#!

We're hiring Software Developers

Click here to find out more

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