liveBook · Manning

Closure

As explained in Julia as a Second Language

A closure is a function that captures external state from its enclosing scope rather than receiving that state as an argument.

Suppose a pipeline needs a function that splits strings on underscores. `split` also needs a delimiter, so it cannot be passed directly into a pipeline that supplies one input at a time. A closure solves this by remembering the delimiter: `splitter('_')` returns a function that accepts only the string. section 15.3.2 section 15.3.1

A closure is a function that captures state from its enclosing scope instead of receiving that state as an argument. In `splitter`, the returned function accepts `str` and uses `dlm` from the surrounding call. Likewise, `mapper(fn)` returns a function that accepts a collection while remembering which function to apply. A closure may be named or anonymous: capturing state, not lacking a name, is what makes it a closure. section 15.3.1

Listing 15.3 splitter and mapper without anonymous functions
"Create a function which splits on `dlm` character"
function splitter(dlm)
    function f(str)
        split(str, dlm)
    end
    return f       #1
end
 
"Create a function applying function `fn` on all its input"
function mapper(fn)
    function g(xs)
        map(fn, xs)
    end
    return g       #2
end

The two returned functions make the distinction visible: each has one explicit input and one captured value. section 15.3.1

Compose a string transformation

In Julia, `f = splitter('_')` and `g = mapper(uppercasefirst)` prepare two closures. Then `"hi_there_world" |> f |> g |> join` produces `"HiThereWorld"`. Each stage receives the previous stage’s result; `f` and `g` retain the extra inputs their operations need. section 15.3.2

Captured state can also stand in for an object’s state. A password-keeper factory creates a dictionary and returns `addlogin` and `getpassword` functions that share access to it. Cipher factories supply interchangeable encryption and decryption functions with the same signatures, so the keeper need not change when the cipher changes. This is a useful contrast with an object-oriented design, not a rule to use closures everywhere: a stateful password keeper may be clearer as objects, while data transformations often suit functions. section 15.7.3

Listing 15.16 A password-keeper service with excessively functional style
function makevault(encrypt::Function, decrypt::Function)
   passwords = Dict{String, String}()

   function addlogin(login::AbstractString, password::AbstractString)
       passwords[login] = encrypt(password)
   end

   function getpassword(login::AbstractString)
      decrypt(passwords[login])
   end

   addlogin, getpassword
end

Follow `passwords`, `encrypt`, and `decrypt` from `makevault` into the returned operations to see how the functions retain what they need. section 15.7.3

How Julia as a Second Language covers it

How deep it goes
Defines captured state and works through named closures, a pipeline, and a stateful service; does not examine runtime implementation.
Examples
Uses delimiter-splitting and mapping functions, a string-transformation pipeline, and an encryption-backed password keeper.
What it assumes
Starts with functions and `map`, showing named and anonymous forms rather than assuming prior knowledge of closures.
What you can do after
Build it from scratch as returned Julia functions that capture configuration or shared state, then judge when objects are clearer.
How it connects
Links anonymous functions, higher-order functions, partial application, function pipelines, and object-oriented programming.

Also explained in

Compare how 20 books explain closure →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Julia as a Second Language. The section links point to the parts of the book it draws on.