There are many reasons to decompose a function into others.
Readability
Which is more readable?
def optimize(quality, rnd, popsize=1000, n_iters):
population = [rnd () for _ in xrange (0, popsize)]
best = None
for _ in xrange (0, n_iters):
best = # 100 lines of code to calculate the best quality
# 100 lines of code to do crossover
# 100 lines of code to mutate individuals
return best
vs
def optimize(quality, rnd, popsize, n_iters):
population = make_population(pop_size, rnd)
best = None
for _ in xrange (0, n_iters):
best, fitnesses = compute_fitness(population, quality)
population = mutate(
crossover(population, rnd), rnd)
return best
def compute_fitness(population, quality):
# 100 lines of code with its own local variables
def crossover(population, quality):
# 100 lines of code with its own local variables
def mutate(population, quality):
# 100 lines of code with its own local variables
The first gives you the big picture view and allows a reader to decide whether and when to dive into the helper functions. If they were inlined that would not be the case.
This is even before you worry about documenting things like contracts. Most structured documentation schemes allow structured documentation of functions, but not of methods or local variables.
Maintainability
If one function has all its single-use dependencies inlined, then a maintainer has to potentially understand all of the local variables before they make any changes. If the function were decomposed, and the maintainer determined that a bug was in one of the helper functions, then they only need to understand the helper function in detail. Which would you rather have to debug?
def longfunction(a, b, c, d, e, f):
# 500 lines of code. Bug manifests somewhere in here.
or
def afunction(a, b, c, d, e, f):
helper1(a, b, c)
# Bug manifests because c is now invalid.
while helper2(c, d, e):
g = helper3(e, f)
h = helper4(a, g)
return helper5(h)
def helper1(a, b, c): # 100 lines of code
def helper2(a, b, c): # 100 lines of code
def helper3(a, b, c): # 100 lines of code
def helper4(a, b, c): # 100 lines of code
def helper5(a, b, c): # 100 lines of code
In the second case, a maintainer stepping through can quickly narrow their search to helper1 and only has to understand in detail 100 lines before coming up with a strategy to fix the bug.
Security
Security is all about isolation -- code is robust against security vulnerabilities when the ability to cause one bit of code to exercise authority in an unintended way does not cause the larger system to exercise authority in an unintended way.
In a language with proper information hiding, you can get security benefits by keeping sensitive objects away from certain code. Object capability languages are designed to make this easy, and often the function is the major means of decomposition.
For example, in an object capability system, if one function delegates a file-system operation to one function and a network operation to another function,
void complexOperation(FileSubtree filetree, Network network, T x) {
helper1(filetree, x);
helper2(network, x);
}
Since no complex code ever gets both the filetree and network, it is much easier to reason about what an attacker has to do to cause files to leak over the network. Since x is the only shared input to the two heplers, if x is immutable before helper1, then it cannot have any state from the file tree that might end up being sent over the network.
Although most languages do not have such fine separation of authority, proper decomposition can make it easier to avoid breaches in mainstream programming languages. With small functions, maybe looking at the call graph can convince you that 90% of the LOC of the system never touch the file-system which means a security auditor can focus on the sensitive 10%. With larger less-granular functions the security auditor will necessarily have to consider more of the program.