Splitting and Merging Functions
The last lesson showed inlining, which dissolves a function into its call site. This lesson generalises the idea: the borders between functions are entirely negotiable. The same computation can live as one large function, or as five small ones calling each other, and the machine does not care which. Splitting carves a chunk of a function out into a new named function. Merging does the opposite. Both preserve behaviour exactly, and both change only one thing: how the code reads.
Think of a long essay. You can present it as one unbroken wall of text or divide it into titled sections. The words, in order, are identical either way. Sections exist for the reader. Function boundaries are the section headings of code, and this lesson is about the tools that add and remove them at will.
Splitting: carving out a step
Splitting, which refactoring tools call extract function, takes a coherent run of statements, moves it into a new function, and replaces the original statements with a call. The variables the chunk read become parameters. The values it produced become return values. Done correctly it is invisible to the machine.
function checkout(cart) {
subtotal = sum_of(cart)
tax = subtotal * 8 / 100
total = subtotal + tax + 5
print "total:", total
}function price(subtotal) {
tax = subtotal * 8 / 100
return subtotal + tax + 5
}
function checkout(cart) {
subtotal = sum_of(cart)
total = price(subtotal)
print "total:", total
}Programmers split functions to make each piece small enough to name honestly. The name price now documents that middle chunk for free. Notice the mechanical care though: the extracted code needed subtotal handed in and its result handed back. Getting that data flow right is exactly what makes splitting a real transformation rather than a copy-paste.
Merging: erasing the seams
Merging runs the other way: fold several functions into one, absorbing each call the way inlining does, until a single function holds the whole flow. Compilers sometimes merge for performance. Obfuscators merge to destroy the reader's table of contents. Here is the same checkout after a merge pass.
function price(subtotal) {
tax = subtotal * 8 / 100
return subtotal + tax + 5
}
function checkout(cart) {
subtotal = sum_of(cart)
total = price(subtotal)
print "total:", total
}function checkout(cart) {
subtotal = sum_of(cart)
tax = subtotal * 8 / 100
total = subtotal + tax + 5
print "total:", total
}Look closely and you will see the merge is the split played backwards, frame for frame. That symmetry is the point of the lesson. Neither direction is more correct. The machine runs the same steps in the same order either way, so the choice of boundaries is free to serve whoever the code is being shaped for: a colleague, or an adversary.
Try it on live code
The playground below holds the merged, one-blob version of a small calculation. Rewrite it as named steps, one intermediate variable per line, in any grouping you like, and run it after each change. As long as the printed output stays the same, your reshaping preserved behaviour, which is the only test that matters. The teaching language has no functions, so variables play the role of boundaries here, and the principle is identical.
The mechanical hazards of boundary surgery are all about data flow. A split must pass in every value the chunk reads and pass out every value later code needs, or it breaks. A merge must rename its way around variable collisions when two function bodies use the same names for different things, the capture problem from the renaming lesson wearing a new hat. Production tools solve both with the same machinery: a symbol table and a tree, never text.
The map and the territory
For a reader, a codebase's function list is a map: names, sizes, and call relationships sketch the architecture before a single body is read. Splitting and merging redraw that map arbitrarily without moving the territory underneath. An obfuscator can merge meaningful units into shapeless blobs, or split one real function into a dozen fragments with meaningless names, scattering a single idea across the file.
And the honest half: the territory is still there. Every statement still executes in the same order, and dynamic analysis, watching the program run, sees straight through boundary games entirely, because execution never cared about the boundaries in the first place. Boundary reshaping taxes static reading. It is one more layer, priced accordingly.
Function borders are presentation. Splitting extracts a chunk behind a new name, merging absorbs callees into the caller, both leave behaviour untouched, and both live or die on careful data-flow bookkeeping. Redrawing the map is a real cost for a reader and no cost at all for the running machine.
Frequently asked questions
Is splitting for readability the same transform obfuscators use?
Mechanically yes, which is the recurring theme of this level. Your IDE's extract function and an obfuscator's function fragmentation are the same tree surgery with opposite naming intent. One splits along the seams of meaning with honest names, the other splits across them with meaningless ones.
Does splitting a function into many small ones slow a program down?
Each boundary adds a call, and calls have small costs, so extreme fragmentation has a measurable price. It is usually modest, and optimizing compilers frequently inline small functions right back, quietly undoing the fragmentation inside the machine while the source text keeps its shape.