Refactoring: Extract Local Lambda

Convert a block in the middle of code into a local lambda

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.

  1. 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. 
  2. 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.

  1. 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. 

  1. Extract Method on a structured block of code. The IDE will warn you if it’s not properly structured.
  1. 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.
  2. 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.

  1. 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 }()
  1. Make any inputs be lambda variables of the same name, and pass them as arguments.
{x in block}(x)
  1. 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.