Go back

Functional Thinking in Go: Insights from Lambda Calculus

Church’s proposal

Alonzo Church, in his foundational work on lambda calculus1, approached the concept of a function in a very general and abstract way. He described a function not merely as a formula or an equation, but as a rule of correspondence: a procedure that takes an input and produces an output. Crucially, Church allowed that the inputs and outputs of a function could themselves be functions. This idea laid the groundwork for what we now call higher-order functions.

Church also introduced the notion of abstraction, a way to define functions without specifying their arguments upfront. In his system, a function of multiple variables could be viewed as a sequence of single-argument functions, a perspective that emphasizes composability and flexibility. While his work was deeply formal and mathematical, the concepts he described — functions as first-class objects, capable of being passed around and combined — are directly relevant to modern programming languages, even those like Go that are not purely functional.

In essence, Church’s work shows us that functions are more than just computations on numbers: they are objects of computation themselves, capable of being manipulated, passed, and composed in powerful ways.

These are the teachings from his work that we can apply in modern programming.

1. Abstraction and Definitions: Clear, Explicit Components

The introduction and use of definitions and other abbreviations is, of course, subject to the restriction that there shall never be any ambiguity as to what formula a given abbreviated form stands for… in practice certain further restrictions are also desirable, e.g., that all free variables of the definiens be represented explicitly in the definiendum.

Church’s schematic definitions allow a single definition to represent a whole class of formulas. In modern programming, this is analogous to generic functions, higher-order functions, or templates:

Schematic definitions in lambda calculus introduce a family of expressions that follow a specific pattern, where each new expression corresponds to a well-formed formula.

2. Bound vs Free Variables: Proper Scope Management

An occurrence of a variable x in a well-formed formula II. is bound or free according as it is or is not an occurrence in a well-formed part of II. of the form (λxN)(λxN)… no occurrence of a variable in a well-formed formula is both bound and free.

Just as Church distinguishes between bound and free variables, programmers should manage variable scope carefully. Local variables, closures, and controlled package-level state prevent unintended side effects and improve clarity.

In programming, this distinction maps directly to scope management:

Closures in Go are a perfect example of how bound and free variables interact cleanly within well-defined scopes.

3. Termination: Ensure Finite Computation

“…if ((λxjNj)Wj)((\lambda x_j N_j) W_j) are parts of 𝐴, then a number 𝑚 can be found such that any sequence of contractions on these parts will terminate after at most 𝑚 contractions… the result is unique up to the order of applying the reduction rules.”

In other words, no matter how complex the formula, applying reductions systematically eventually produces a final, stable result. There is no possibility of an infinite loop within the formal system—termination is guaranteed.

In programming, this principle has a clear analogue: computations should eventually complete. Unbounded recursion, infinite loops, or unending asynchronous processes violate this principle and make programs unpredictable or unsafe. To ensure termination in modern software:

Practical Abstraction in Go: Higher-Order Functions and Composable Patterns

Church states that schematic definitions are subject to restrictions that prevent ambiguity, such as ensuring all free variables of the definiens (the body of the definition) are explicitly represented in the definiendum (the name/placeholder).

In the lambda-calculus, a schematic definition allows a single placeholder to represent a whole family of formulas (expressions) that follow a specific pattern.

This process is the direct analog of creating reusable, type-safe components in modern code:

  1. Parametric Abstraction: Just as Church uses placeholders like MM and NN to stand for any well-formed formula, modern programming uses generics (e.g., Go’s [T any]) or higher-order Functions to abstract over specific data types or behaviors while preserving the operation’s structure.
  2. Explicitness (Safety): Church’s restriction on free variables ensures you know exactly what you are defining. In programming, this means explicitly defining all parameters and type constraints in the function signature. This prevents silent side effects or ambiguity, which is the cornerstone of robust software design.

Instead of trying to implement Church arithmetic, which relies on a deep functional encoding, a better Go example illustrates a simpler, clearer schematic pattern: Function Composition.

Go Illustration: Implementing the Compose Schema

The schema for composition is simple: apply function FF to the result of function GG.

[FG](λx.(F(Gx)))\begin{aligned} [F \circ G] \longrightarrow (\lambda x. (F \, (G \, x))) \end{aligned}

This single schematic definition applies to any two functions FF and GG whose types align.

package main

import "fmt"

// The Definiens: The generic Compose function (F o G)
// It abstracts over the types of the two component functions (F and G).
// F and G are the "explicit free variables" in the Church schema.
func Compose[A, B, C any](F func(B) C, G func(A) B) func(A) C {
    // The implementation (the Definiens):
    // Returns a new function that represents the composition logic.
    return func(x A) C {
        return F(G(x))
    }
}

// Concrete Functions (Formulas) to substitute into the schema
func AddTwo(y int) int { return y + 2 }
func Double(z int) int { return z * 2 }

func main() {
    // We create a new, well-formed formula (function) by applying the schema.
    // The resulting function is DoubleThenAddTwo, which is (AddTwo o Double)
    // Here, AddTwo is F, and Double is G.
    DoubleThenAddTwo := Compose(AddTwo, Double)

    // The result is predictable and unambiguous, adhering to Church's rules.
    fmt.Println("Result of (AddTwo o Double) on 5:", DoubleThenAddTwo(5))  
    
    // We can use the same schema to define the reverse composition (Double o AddTwo)
    AddTwoThenDouble := Compose(Double, AddTwo)
    fmt.Println("Result of (Double o AddTwo) on 5:", AddTwoThenDouble(5)) 
}

Church’s lambda calculus teaches us the power of abstraction, higher-order functions, and composability. The Go Compose function takes that lesson and turns it into something immediately usable: a clear, type-safe, and reusable building block.

Conclusion

Church’s lambda calculus might seem abstract and mathematical, but its lessons are surprisingly practical. By thinking of functions as first-class objects, paying attention to variable scope, and designing predictable, composable patterns, we can write clearer, safer, and more reusable code.

In Go, these ideas show up as higher-order functions, generics, and function composition—tools that help us build programs that are easier to reason about and maintain. The takeaway is simple: even if you’re not writing purely functional code, understanding these principles can make you a better programmer, because good abstractions and clear structure always pay off.


Footnotes

  1. Church, A. (1941). The Calculi of Lambda-Conversion. Princeton: Princeton University Press.


Share this post on:

Next Post
Building an xDS Control Plane in Python: Embracing Dynamic Networking