Skip to content

Instantly share code, notes, and snippets.

@joshuabowers
Last active January 5, 2024 23:08
Show Gist options
  • Save joshuabowers/c6500f7640e234b1a671df52f916cddc to your computer and use it in GitHub Desktop.
Save joshuabowers/c6500f7640e234b1a671df52f916cddc to your computer and use it in GitHub Desktop.
type_fn: 004 - Closures

A scope is the set of all data which is available to a given context. A new, fresh scope is constructed for operational contexts—classes and functions—which allows those contexts to define their own data without polluting other scopes.

Within a JavaScript function, this would include all classes, functions, and variables which are defined directly within the function. TypeScript augments this with all type definitions made directly within the function.

However, an operational context does not have access to just its own scope; instead, it folds in access to the scope within which it was created, giving it access to the classes, functions, variables, and types that were known at its point of definition. Should a function make use of this outer scope, it is said to be a closure, for in uses the enclosing data.

Closures can provide a great degree of power, especially when used in the context of higher order functions and continuations, but can also prove to be stumbling blocks. In TypeScript, as in many languages, this is due to the enclosing scope being alterable.

When a function defines a new variable, that variable definition gets added to its scope. This can shadow similarly named variables defined within the outer scope, which is often desirable. (Programmers, being lazy efficient folk, tend to reuse identifiers for things that are roughly similar.) The variables defined locally—within the most recently defined scope—will behave according to that scopes restrictions, rather than how their outer context constrained them.

But should an outer scope variable be used without redefinition, its current value at the time the inner scope is evaluated will be used. Furthermore, the inner scope could, intentionally or not, modify variables defined in an outer scope, impacting contexts outside the function.

These state mutation concerns are typically considered undesirable side effects. Many developers strive, when programming functionally, to avoid side effects. Functions which do not edit outer scope state are said to be pure. This need not constrain the use of enclosing context, but it does require the developer to be cognizant of it and how they are interacting with it. Generally, this typically means inserting external state via extra parameters, or enclosing an immutable copy of the external state via a higher order function.

Pure functions, which do not alter state nor rely upon alterable state, are effectively idempotent: for the exact same inputs, they should return the exact same result. Sure functions are vastly easier to reason about and verify the correctness of their algorithms via testing.

// Define a type, Binary, for a function which takes two numeric
// parameters and returns a number
type Binary = (a: number, b: number) => number
// Provide some mutative state in the module's scope, which more
// local scopes might use and abuse.
let a = 5;
// Two functions which redefine, or shadow, the outer scope's definition
// of `a` with their own local variant. No matter how many times these
// functions are invoked, their copy of `a` is unique to that invocation.
// Also note that despite having similarly named parameters, both functions
// have effectively distinct, unique variables.
const add: Binary = (a, b) => a + b;
const multiply: Binary = (a, b) => a * b;
// Adds the value of `a` at the time this is executed
const calculate = (b: number) => multiply(add(a, b), a)
// Danger! Non-pure function: this has a side effect of updating `a`
// from the enclosing scope with the value of `b`.
const mutate = (b: number) => a = b;
// At this point, `a` is 5, so `first` will be `75`.
const first = calculate(10)
// However, we next mutate `a` via a side effect to be `1`...
mutate(1)
// And now rerun `calculate` with the exact same argument, and its
// result value, in `second` is `11`.
const second = calculate(10)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment