3 Comments

Folding Away Mutations in F#

I’ve been working in F# lately, and one of its smaller features has had a big impact on my programming style: Variables are immutable by default. If you want to change something, you have to declare it mutable. Sometimes, this is what I naturally reach for, and the language rubs my nose in it.

Here’s a story of one of those instances, and a new pattern I’ve picked up as a result.

The Task

So, I had this object and a list of operations to perform on it. For this post, we’ll apply a series of text replacements to a string. Here’s an imperative for loop:


let performReplacements (input:string) (replacements: (string*string) list) =
    let mutable text = input
    for replacement in replacements do
        text <- text.Replace(fst replacement, snd replacement)
    text

That mutable keyword (and the <- assignment operator) stick out like sore thumbs. How could we avoid them?

Fold

Depending on your language background, you may not have encountered an operation called fold before. I hadn't! And it turns out, this unfamiliarity proved beneficial, allowing me to approach it with an open mind rather than with the preconceived notions I'm about to describe.

I'm familiar with some of fold's other names: it's more often known as reduce(), and for some reason it's aggregate() in C#.

The single-parameter version collapses a list of type X into a single value of type X:


[1,2,3].reduce((a,b) => a+b) // 6

The two-parameter version threads an accumulator through a series of operations:


(new List{1,2,3,4,5})
    .Aggregate(seed: "", func: (string accum, int item) => accum + item.ToString()) // "12345"

I tend to think of this second form as merely an alternate of the first: a mechanism for deriving one value from a list of inputs. The seed parameter, then, is just an empty version of the desired output structure, and I always wind up supplying a constant.

This is a limited way of thinking! Instead, what if we think of the seed as an input?


let n = numberFromSomewhere ()
let result = [2;3;4] |> List.fold (*) n

Here we start with a number, perform several multiplications, and get a result, with nary a mutable! This sounds familiar.

Back to the Problem

Applying this technique to the initial problem, we get something like this:


let performReplacements (input:string) (operations: (string*string) list) : string =
    operations |> List.fold (fun accum (oldValue, newValue) -> accum.Replace(oldValue, newValue)) input

I was feeling ashamed of my imperative code, and this is way more functional. It's a trivial example, but I've found the pattern to be helpful for larger, more complex chunks of code where mutability presents more risk and cognitive overhead.

Has learning a language ever taught you something new about a concept you thought you already knew?

Further Reading: