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.
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
bind
returns a new function with the same body but with a fixedthis
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 areturn
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.