Extracting a local lambda turns a block of code into an anonymous inline function.
Motivation
A local lambda can isolate code for improvement (the Isolate-Improve-Inline pattern – see References), or be the first step in using a lambda another way.
- Isolate: A local lambda is almost as isolated as an extracted method. You can rename variables without affecting the containing method, rearrange conditionals, extract methods, and so on.
- Reduce Dependencies: If you want another class to call a chunk of your code, you’ve got to put it somewhere. If you provide a method, the caller has to know the class (or protocol/interface), and has to know the name of the method.
By contrast, a lambda is anonymous – it has no name. The caller chooses a name to refer to the lambda, and can call it or pass it along. There’s less dependency – less the caller has to know.
- Rearrange Code: Once you have extracted the lambda, you can split the creation of the lambda from its use (call). You might use the lambda as a strategy, or perhaps the lambda is part of a series of transformations that can be put in an array for flexible reordering.
Structured Blocks
When you have a chunk of code to extract, whether as a lambda or a method, you have to consider control flow and data flow.
Control Flow
A structured block must have one entry and one exit. It may contain properly nested loops or conditionals. If your code has a guard clause that exits the whole method, it’s not a candidate (unless you’re extracting the whole method).
This usually isn’t an insurmountable barrier – you can transform a multi-exit block to a single-exit block by using flags, states, and/or reworking the structure.
Data Flow
Identify the “gozindas and gozoutas” (“goes into” = input, and “goes out of” = output). Some variables may be both, e.g., a variable defined before our block and read after it. Inputs will become parameters, and outputs will form the return value.
Some languages only allow a single return value; you’ll either have to introduce a type to hold multiple values, or use a different approach.
Some variables may not be able to be inputs or outputs. For example, C’s volatile variables may have real-world side effects when read or written. You can’t safely optimize “x = 3; x= 4” to just “x=4”. Be very careful with volatile variables to make sure you’re not creating new reads or writes, or deleting existing ones.
Mechanics – Mostly Automated
I use this approach when the block of code is complicated enough that I need help understanding the control or data flow.
- Extract Method on a structured block of code. The IDE will warn you if it’s not properly structured.
- If any arguments are in-out or passed by reference, change them to accept the value as input and return it as one of the outputs.
- Replace the method call with the body of the method and call it immediately.
- Pass inputs as arguments
- Assign outputs to the appropriate variables
Thus: (a,x) = f(x,y)
becomes (a,x) = { body }(x,y)
Example, in Swift – If our block is:
x = x + 3
y = 21 * y
Extract to a function:
func extracted(_ x: inout Int, _ y: inout Int) {
x = x + 3
y = 21 * y
}
extracted(&x, &y)
Convert in-out parameters to explicit input and output: (We have to create extra variables since Swift doesn’t allow parameter variables to mutate.)
func extracted(_ xIn: Int, _ yIn : Int) → (Int,Int) {
let x = xIn + 3
let y = 21 * yIn
return (x,y)
}
(x,y) = extracted(x,y)
Replace the method call:
(x,y) = { xIn, yIn in
let x = xIn+3; let y = 21 * yIn; return (x,y)
}(x,y)
Mechanics – Manual
When it looks like an easy transformation, I tend to do it manually.
- Wrap the block in a lambda and call it. (We’ll have syntax errors and won’t work properly until we complete step 3.)
{ block }()
- Make any inputs be lambda variables of the same name, and pass them as arguments.
{x in block}(x)
- Assign any results to the corresponding variables.
x,y = { x in block}(x)
4. Everything should compile, and tests should pass.
Be very careful about outputs. If a variable is defined before and used after the code you extract, the compiler won’t notice that you’ve changed the meaning.
Further Notes
I think of this as the original refactoring – take an expression with “free” (externally defined) variables and make them into lambda variables, thus turning it into a function. This is a core idea in lambda calculus, defined in the 1930s.
Jay Bazuzi describes how to “Safely extract a function in any C++ code” (see References). His approach is similar to the “Mechanics – Manual” above, followed by a step to extract a separate function.
Summary
We considered the idea of structured blocks, then defined a couple ways to extract them into a local lambda. That lambda gives you more freedom to improve its code, and to reorganize how you use it.
References
“Isolate-Improve-Inline: The 3-I Refactoring Tactic”, by Bill Wake.
“Lambda for Control Structures, a Refactoring“, by Bill Wake.
“Lambda to Reduce Coupling“, by Bill Wake.
“Safely extract a function in any C++ code”, by Jay Bazuzi.