0

I am trying to understand what can be done with closures that would be difficult or impossible to do without closures.

One use of closures I have found is to make an object's variables private, for example:

function CreateEmployee()
{
    var name;
    
    var obj = 
    {
        "getName": function()
        {
            return name;
        },
        "setName": function(newName)
        {
            name = newName;
        }
    };
    
    return obj;
}

var e1 = CreateEmployee();
e1.setName("John");
alert(e1.getName());

var e2 = CreateEmployee();
e2.setName("Christopher");
alert(e2.getName());

In this example, the variable name becomes effectively private. But if JavaScript allowed for explicitly making an object's variables private, then we wouldn't need to use closures in the first place!


Another use of closures I have found is to pass a variable to an asynchronous function, for example:

function foo(i)
{
    function foo2()
    {
        alert(i);
    }
    
    setTimeout(foo2, 3000);
}

foo(10);

foo(20);

In this example, two timeout events will be created, and each timeout event have its own copy of the i variable, and after 3 seconds, the JavaScript engine will execute the functions associated with these timeout events.

But JavaScript could have allowed us to do this without the use of closures. For example the setTimeout() function could have been implemented in a way that allows us to pass a variable to it, and this variable will later be passed as an argument to the function associated with the timeout event that the JavaScript engine will execute, for example:

function foo2(i)
{
    alert(i);
}

var i = 10;
setTimeout(foo2, 3000, i);

i = 20;
setTimeout(foo2, 3000, i);

And when the 3 seconds are passed, the JavaScript engine would simply do the following:

foo2(10);

foo2(20);

Can someone provide an example that shows what can be done with closures that would be difficult or impossible to do without closures?

5
  • One possible example is "writing spaghetti code" - it would be difficult or impossible to have written your spaghetti code, without closures!
    – Steve
    Jul 16, 2022 at 21:07
  • 3
    @Steve: Oh, ye of little faith. How else do you think we wrote spaghetti code before closures? We just wrote it! Jul 16, 2022 at 23:11
  • @GregBurghardt, I wasn't talking about any spaghetti code. Only this brand of it! You'd need a computer to work out how i moves around. Anonymous methods are a true boon. But implicit variable capture is an abomination against all things - ...in my view!
    – Steve
    Jul 17, 2022 at 11:46
  • Mind you, on a more sober note, I suppose variable capture is useful in some circumstances - for example, for the Linq functionality in .Net. But there is a definite need to ensure locality of use for things to remain comprehensible. For example, it should not be possible to use implicitly captured variables outside the scope in which they were declared - in the above example, i is not in scope of the inner method, as the callback from setTimeout is asynchronous and occurs after the scope of 'i' has already closed.
    – Steve
    Jul 17, 2022 at 11:59
  • The point of such a feature isn't that there is no replacement for it. I can perfectly write programs in Javascript or other languages without the for loop feature by using only "while" loops, or "goto" loops. The point of such a feature is that it allows to introduce more structure, or more explicit and useful structure into programs.
    – Doc Brown
    Jul 20, 2022 at 14:54

1 Answer 1

6

Anything you can do with closures you can also do with objects, and vice versa. At least if no static type system gets in the way. They two concepts are fundamentally equivalent.

Before Java got closures, it was normal to implement callbacks as anonymous classes with a single method, which looked like:

interface Callback {
  void call(int data);
}

int captured = 42;

functionWithCallback(
  // anonymous class captures referenced arguments by value
  new Callback() {
    void call(int argument) {
      System.out.println("got argument=" + argument
                        + " and captured=" + captured);
    }
  }
)

A couple of years ago I wrote a blog post on implementing an object system via closures in JavaScript, based on the idea that you can express object property access object.method_or_field as a function call object_like_closure("method_or_field").

Representing closures in C

Under the hood, objects and closures are generally implemented similarly. Both need to reference some runnable code and need a structure for the owned data. In C, a data structure for a closure/object might look like:

struct CodeAndData {
  void (*code)(void* data, int argument);
  void *data;
}

To call this closure, it is necessary to give it access to the data:

struct CodeAndData closure = ...;
closure.code(closure.data, 123);

To create a closure, we must provide an appropriate function. The equivalent to the above Java code would be:

void functionWithCallback(CodeAndData callback);

static void callback_code(void* data, int argument) {
  int captured = *(int*) data;  // cast the data pointer
  printf("got argument=%d and captured=%d\n", argument, captured);
}

int captured = 42;
CodeAndData callback = {
  .code = callback_code,
  .data = &captured,  // manually capture relevant data
};
functionWithCallback(callback);

So it is possible to implement closures even in languages that do not support them natively, as long as we have function pointers and a flexible type system.

As a practical example of this pattern, consider the qsort() function in the C standard library. It implements a quicksort algorithm over arbitrary data, and uses a function pointer to compare elements.

  • The basic qsort() function has signature

    void qsort(void *ptr, size_t count, size_t size,
               int (*comp)(const void *, const void *) );
    

    Here, the function pointer only receives pointers to the elements it is currently comparing. If it needs access to external data, it would have to use global variables.

  • The qsort_s() (C11) or qsort_r() (POSIX) function adds a data pointer, which will be passed to the callback as a third argument:

    errno_t qsort_s(void *ptr, rsize_t count, rsize_t size,
                    int (*comp)(const void *, const void *, void *),
                    void *context );
    

    This is exactly the solution you proposed with setTimeout(). This is also equivalent to my CodeAndData struct, except that this function uses separate variables for the function pointer and the data instead of a single struct.

Representing objects in C

On this level, it's worth noting that functions and objects are often implemented slightly differently: a core feature of objects is that they are recursive, so that a method can call other methods on the same objects. If our CodeAndData were more object like, then its definition and the calling convention would change to:

struct CodeAndData;  // forward declaration

struct CodeAndData {
  void (*code)(CodeAndData* self, int argument);
  //           ^^^^^^^^^^^^^^^^^
  void *data;
}

And instead of a single function-pointer and a data-pointer, it's common to pass a vtable pointer that can contain multiple methods with different signatures, and to embed the captured data directly into the structure. For example, the C++ class

class Foo {
  int some_field;
  int another_field;

  virtual int methodA() const;
  virtual void methodB(int);
}

...
Foo* f = ...;
f->methodB(123);

Would probably be layouted like

struct Foo;

struct VtableFoo {
  int (*methodA)(Foo const* self);
  void (*methodB)(Foo* self, int);
}

struct Foo {
  VtableFoo const* vtable;
  int some_field;
  int another_field;
}

...
Foo* f = ...;
f->vtable->methodB(f, 123);

A possible methodB implementation that makes use of this ability to invoke methods might look like:

void Foo_methodB(Foo* self, int argument) {
  int result = self->vtable->methodA(self);
  self->some_field = result + argument;
}

As a practical example, this pattern is used throughout the Linux kernel, in particular for the file system (see fs.h). An open file might be a network socket, or a pipe, or a file on an Ext4 filesystem, or many other things. To manage this flexibly, there is a file_operations struct with tons of function pointers, and the file struct points to matching file_operations:

struct file_operations {
  ssize_t (*read)(struct file*, char*, size_t, loff_t*);
  ssize_t (*write)(struct file*, const char*, size_t, loff_t*);
  ...
};

struct file {
  const struct file_operations* f_op;
  ...
};
5
  • On the note of C: A lot of C APIs simulate closures, by taking a callback, and a void *userInfo (or context, etc.). When the desired event happens, the API will call your callback, passing you back the userInfo. You can cast it, and access whatever goodies you stashed for yourself. This is exactly like the context capturing done by closures, except not automatic.
    – Alexander
    Jul 17, 2022 at 0:31
  • @Alexander good point, I edited the answer to mention the relevant variants of the qsort() function in the C/POSIX standard library.
    – amon
    Jul 17, 2022 at 10:18
  • Ah yes, that’s a perfect example.
    – Alexander
    Jul 17, 2022 at 22:37
  • IMHO your blog posts misses a reference to SICP, which presented the idea of using closures for implementing object systems 20 years earlier.
    – Doc Brown
    Jul 20, 2022 at 14:57
  • @DocBrown Of course SICP was influential for that post. SICP does present a short example of data guarded by functions in section 3.2.3 for introducing closures, and my Pair() example is a direct nod to Lisp's cons. However, my post goes way beyond that and presents approaches for implementing OOP concepts like subclassing and open recursion, which SICP ignores. My approach probably isn't novel, but it's not based on SICP in any meaningful sense.
    – amon
    Jul 21, 2022 at 13:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.