Concepts / Function Objects and Callable Values

Function Objects and Callable Values

The value stored in cube2 is a callable object that holds the original function and the bound value together. The 2 is decided when the object is created, not when it is used.

  • Programming

A Function That Has Not Run Yet

Read the short program below once, slowly, and notice which name appears on each line. The first line declares a function called cube that takes a double. The second line uses bind to produce a second name, cube2, from cube and the value 2. Everything in this article is about what that second line creates and what happens when you use it.

python
Notes on the code
  • Line 1 declares the function cube, which takes one double
  • Line 2 is the body of cube; it is shown only to make the example concrete
  • Line 4 uses bind to build cube2 from cube and the value 2

What do you think happens?

After the last line runs, has cube been called with the value 2 yet?

Reveal answer
Answer:

No. bind() does not call cube. It creates a new function object, and cube only runs when cube2 is called.

The source is explicit that the call happens when the new object is called, not when bind() is written. The line only produces a value that can be called later.

The code above uses the word bind as the operation the source describes. It is pseudocode for that idea rather than a call to a specific library function, so check your own language's documentation for the exact name and syntax it uses.

What bind() Actually Produces

Definition

bind() creates a new function object from an existing function and the arguments you choose to bind.

The important word in that definition is create. bind() does not run cube, and it does not print or return a number on its own. It produces a new callable object, which is the value stored in cube2. You can think of that result as a packaged request: the original function, together with a value that has already been decided. Nothing inside the packaged request runs until someone calls it.

cubeoriginal functionbind()creates, does not callcube2new function object2bound argumentfunctionchosen argumentreturns
What is the relationship between the original function, the bound argument, and the new function object that bind() returns?
Key point

The value stored in cube2 is a callable object that holds the original function and the bound value together. The 2 is decided when the object is created, not when it is used.

Tracing One Call to cube2

The source states the behavior directly: a call cube2() invokes cube with the argument 2, which means cube(2). The diagram follows the order of events. The calling code asks cube2 to run. The cube2 object then invokes the original cube, supplying the argument that was bound earlier. The caller never has to write the 2 again, because the value lives inside the new function object.

Calling codewrites cube2()cube2holds bound value 2cubeoriginal function1. call cube2()2. invoke with 23. result returned4. result returned
When I call the new bound object, which function actually runs, in what order, and where does the bound argument get inserted?
Worked example

Calling cube2 with no arguments

The calling code writes cube2() with nothing inside the parentheses. What runs, and what value flows back?

  1. 1
    Step 1: the call arrives at cube2

    The caller provides no argument, because the only parameter of cube has already been bound.

  2. 2
    Step 2: cube2 supplies the bound value

    The object invokes the original cube and passes the value 2 that was stored when bind() ran, so the call is cube(2).

  3. 3
    Step 3: the original function runs

    cube runs with x equal to 2 and computes its result, which is 8 for the cube of 2.

  4. 4
    Step 4: the result returns to the caller

    The value produced by cube flows back through cube2 to the line that made the call.

Result

cube2() returns the same value as cube(2), which is 8.

Binding Some Arguments, Not All

So far the function has had one parameter, and that one parameter was bound. The source makes a broader point: bind() can bind some or all of a function's arguments, and you do not have to bind every argument. A bound argument is fixed in advance. An argument you leave unbound is still a parameter of the function, which means whoever uses the result must still supply it.

Consider this illustration, which is generated for teaching and is not taken from the source. Suppose a function takes two doubles, a factor and a value, and returns their product. If you bind only the factor, you have fixed one piece of the call and left the other open. The resulting object is still waiting for the value that was not bound. If you bind both, nothing is left open. The two situations differ only in how many arguments were fixed when the object was created.

python
Output (expected)
15
Notes on the code
  • multiply has two parameters, both still open
  • bind fixes only the first parameter, factor, to 3
  • triple(5) supplies the one remaining argument, value, as 5
  • The call behaves as multiply(3, 5), which gives 15
factorbound to 3triple(5)one argument givenvalueopen, caller supplies 55 fills next open slot3 already fixed
If I bind only the first of three parameters, how do the remaining caller-supplied arguments map onto the original function's remaining positions?

Where Unexpected Results Come From

When a result from a bound function is not what you expected, the most useful first question is where the value came from. A bound function has two places where behavior is decided: the line that creates the object and the line that calls it. A value that was fixed at creation time can differ from the value you assumed when you wrote the call.

Debugging

Suppose a program binds a variable's current value into a function, then later changes that variable and calls the bound function. The bound function still uses the value that was fixed when bind() ran. Example, generated for teaching: factor is 3 when scale = bind(multiply, factor) runs. Afterwards factor is changed to 4. Calling scale(5) still gives 15, because the 3 was fixed at creation time and the later change to factor does not reach into the bound object.

  • Common mistake

    Expecting bind() to run the original function

    What it looks like

    Writing cube2 = bind(cube, 2) and assuming a value appears on that line.

    Why it fails

    bind() only creates the new function object. The original function runs when the new object is called.

    Do this instead

    Read the bind line as creating a callable, and look for the call that actually runs it.

  • Common mistake

    Assuming a bound value is looked up again at call time

    What it looks like

    Changing a variable after bind() and expecting the bound function to use the new value.

    Why it fails

    The value is fixed when the object is created, so later changes to the variable do not affect the bound result.

    Do this instead

    Trace the value back to the bind line, and check what it was at that moment.

  • Common mistake

    Forgetting that unbound arguments are still required

    What it looks like

    Binding only the first of two parameters and calling the result with no arguments.

    Why it fails

    An unbound argument remains a parameter of the function, so the caller must still supply it.

Practice Tracing Bound Calls

MEDIUM

A function called power takes two parameters, a base and an exponent. A new object is created with exponent_two = bind(power, base_unchanged...). Instead of that line, suppose you write square = bind(power, 3), which binds only the first parameter to 3. Then you call square(2). Which parameter is fixed, which one is still open, and what value does the open parameter receive?

Hints
  • List the parameters of power in order, first then second.
  • Mark the first parameter as bound to 3 and the second as open.
  • The caller's argument fills only the open position.

Key Takeaways

  • bind() creates a new function object from an existing function and the arguments you choose to bind; it does not call the original function.
  • A call to the new object invokes the original function with the bound arguments, so calling cube2() runs cube(2).
  • You do not have to bind every argument. Bound arguments are fixed at creation time, and unbound arguments remain parameters the caller must supply.
  • When a bound function gives an unexpected result, check the value that was fixed at the bind line, since that is where the behavior was decided.

Related concepts

More concepts starting with F