Article summary
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:
- Functional Programming vs. Imperative Programming
- Getting Started with F#
- A simple explanation of fold, and two less simple ones.
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.
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))
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 😱