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 &lt;- 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:

Conversation
  • Lewis says:

    Fold and Reduce are actually 2 slightly different functions (at least in the F# world)

    Fold takes a seed value, whereas Reduce does not.
    Reduce will throw an exception when given an empty list, and Fold will return the seed value.

    Other languages tend to have one or the other with an overload; for example C# has 2 overloads of Aggregate for this reason.

  • Rex says:

    A more idiomatic way to write this fold is to pattern match on the op tuple:

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

    Or even more succinct, eta-reduce the whole thing:

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

  • John Ruble John Ruble says:

    Thanks for the comment, Rex. Good calls on both points. I updated the final example to use tuple pattern matching, but will hold off on partial application to be a little more beginner-friendly.

    The situation that led to this post was more like this:

    let result, errors = operations |> Seq.fold (mutationFn param1) (inputObject,[])

    ..and the mutation was reflection 😱

  • Comments are closed.