Callable Objects: State of the Art

Although function value assignment is not a feature we can found in most languages, most of them provide a way to create callable objects.

Most nowadays versions of mutable programming languages like C++, JavaScript (Eloquent JavaScript), Lisp, Smalltalk, and many others allow the creation of callable objects.

📚

A callable object is a data structure that behaves as both an object and a function. The idea of Callable Object was introduced by Ben Bruun Kristensen et al. for the influential Beta language in 1983 (Kristensen 1983) and later recounted in a HOPL III paper in 2007 (Kristensen 2007). The slogan of Beta could be “Everything is a Pattern”, where “Pattern” is the datatype of explicit callable objects.

Many languages since then have adopted the idea of callable objects, including JavaScript, Python, and C++. Python is one of the many languages that provides callable objects. In Python, any object can be called if it defines a __call__() method. C++ has also callable objects through the use of functors (function objects). In C++, any object that overloads the operator () can be invoked as if it were a function.

modifiable-functions-via-callable-objects.js
class Callable extends Function { // Instances are callable just like a function
    constructor() {
        super('...args', 'return this._bound._call(...args)') // Create a new function with the body "return this._bound._call(...args)"
        this._bound = this.bind(this) // Bind the instance to the just built function and store it as a property
        return this._bound            // Return the bound function instead of the instance
    }
}
 
class CachedFunction extends Callable {
    constructor(f) {
        super()
        this.function = f; this.cache = new Map();
    }
 
    _call(arg) {
        if (typeof this.cache.get(arg) !== 'undefined') return this.cache.get(arg);
        return this.function(arg);
    }
}
 
let cf = new CachedFunction(x => x*2);
console.log(cf(1));  // 2 since 1 * 2 is 2
cf.cache.set(1, -1); // Low level version of cf(1) = -1
console.log(cf(1));  // -1 
console.log(cf(0));  // 0 fixed bug for falsy values. Commented by Boriel
cf.cache.set(0, -2); // Low level version of cf(0) = -2
console.log(cf(0));  // -2

Listing above from repo ULL-ESIT-PL/callable-objects provides a simplified example of how to create modifiable function objects using callable objects. It is written in JavaScript but can be translated to any other language having this feature. In fact, our implementation of the proposed functionality is going to be based on callable objects and proxies.

The Callable class implements a pattern that allows instances of the class to be directly callable as functions. It does so by extending the built-in Function class allowing us to create instances that behave like functions, but behind the scenes, can still have class-like properties and methods.

When you instantiate this class with new Callable(), you get back a function that, when called, will invoke the _call method on the original instance.

The code uses two features: the bind method and an explicit return statement in the constructor:

  • The bindreturns a new function with the same body but with a fixed this value:

    > p = {x:1, s: function() { console.log(this.x) } }
    > p.s()
    1
    > f = p.s 
    > f() // uses the global object as "this" and the global object has no x property
    undefined
    > g = f.bind(p) // g is a new function with "this" fixed to p
    > g()
    1
  • In JavaScript, constructors typically don’t need explicit return statements because they automatically return the newly created instance (this). However, including a return statement in a constructor has special behavior that can be useful in certain patterns. When, as above, the constructor returns an object, that object replaces the instance that would normally be returned

For this to work, you would need to implement a _call method in a subclass as is done in the CachedFunction subclass, as that’s what will be invoked when the callable instance is called.

The CachedFunction class extends Callable and implements a caching mechanism.

It stores function results in a Map for efficient retrieval. If a value has already been computed for a given argument, the cached value is returned. Otherwise, the original function is invoked. A CachedFunction object behaves like a modifiable function since it can be “manually” modified using cf.cache.set(key, value).

Function modification is already available in modern programming languages through mechanisms like callable objects, but the goal of the left-side lab is to raise the level of abstraction of our “calculator language” allowing function modification. Obviously, everything that can be done with function expressions on the left side can be done with callable objects.

Many authors have argued (Backus, Hoare, Hudak et al., Dijkstra) that assignments and state mutations are problematic constructs. The misuse of assignments in some contexts, like concurrent and parallel programming, can lead to problems. Assignment introduces time and state into programs, making reasoning about program behavior more difficult Sussman. Avoiding assignment through immutability and functional patterns usually leads to safer and more predictable code, but assignment is spread over a galaxy of mutable programming languages, and there are tasks for which it remains a useful and effective tool. It is for this reason that we are going to explore this idea in this lab.

References

  • Kristensen, B. B., Madsen, O. L., Møller-Pedersen, B., & Nygaard, K. (1983). Abstraction mechanisms in the Beta programming language.
  • Kristensen, B. B., Madsen, O. L., Møller-Pedersen, B., & Nygaard, K. (2007). The Beta programming language. HOPL III.
  • Backus, J. (1978). Can programming be liberated from the von Neumann style? A functional style and its algebra of programs. Communications of the ACM, 21(8), 613-641.
  • Hoare, C. A. R. (1985). Communicating sequential processes. Prentice-Hall.
  • Hudak, P., Hughes, J., Peyton Jones, S., & Wadler, P. (2007). A history of Haskell: being lazy with class. In Proceedings of the third ACM SIGPLAN conference on History of programming languages (pp. 12-1).
  • Dijkstra, E. W. (1968). Go to statement considered harmful. Communications of the ACM, 11(3), 147-148.
  • Sussman, G. J., & Steele Jr, G. L. (1975). Scheme: An interpreter for extended lambda calculus. Higher-Order and Symbolic Computation, 11(4), 405-439.