Function Pointers

Remember nested C language declarators of death needed to describe a pointer to a function, which returns a pointer to a function, which returns yet another pointer, and so on?

Nested declarators are evil! Fortunately, there are other ways to achieve the same result. Jancy uses a different approach, which is much easier to read while allowing to declare function pointers of arbitrary complexity.

void foo() {
    // ...
}

int* bar(int x) {
    // ...
}

int* function* chooseFunc()(int) {
    return bar;
}

void main() {
    function* f() = foo; // boooring!
    int* function* f2(int) = bar;
    int* function* function* f3()(int) = chooseFunc; // keep going...
    int* function* function* f4()(int) = chooseAnotherFunc;
    int* function* function** f5[2]()(int) = { &f3, &f4 }; // oh yeah!

    (*f5[0])()(100); // bar(100)
}

Function pointers can be normal (fat) or thin. thin pointers are just like C/C++ function pointers: they simply hold the address of the code.

void foo(int a) {
    // ...
}

void bar() {
    function thin* p (int) = foo;
    p (10);
}

Unlike C/C++, the argument conversion is automated (Jancy compiler generates thunks as needed)

void foo(int a) {
    // ...
}

void bar() {
    typedef void FpFunc(double);

    // explicit cast is required to generate a thunk
    FpFunc thin* f = (FpFunc thin*)foo;

    f(3.14);
}

The true power comes with fat function pointers. Besides the code address, fat pointers also hold the address to the closure object, which stores the context captured at the moment of creating the function pointer.

class C1 {
    void foo() {
        // ...
    }
}

void bar() {
    C1 c;

    function* f() = c.foo; // in this case, pointer to 'c' was captured
    f();
}

Jancy also allows to capture arbitrary arguments in the closure through the use of partial application operator ~()

void foo(
    int x,
    int y
) {
    // ...
}

void bar() {
    function* f(int) = foo~(10);
    f (20); // => foo(10, 20);
}

You are free to skip arguments during the partial application. For example, you can make it so that the argument 3 comes from the closure, while arguments 1 and 2 come from the call.

class C1 {
    void foo(
        int x,
        int y,
        int z
    ) {
        // ...
    }
}

void bar() {
    C1 c;

    function* f(int, int) = c.foo~(,, 300);
    f(100, 200); // => c.foo(100, 200, 300);
}

Fat function pointers can be weak, meaning they do not retain some of the objects in the closure.

class C1 {
    void foo(
        int a,
        int b,
        int c
    ) {
        // ...
    }
}

void bar() {
    C1* c = new C1;

    function weak* w(int, int) = c.foo(,, 3);

    // uncomment the next line and C1 will get collected next gc run
    // c = null;

    std.collectGarbage();

    function* f(int, int) = w;
    if (f) {
        // object survived GC run, call it
        f(1, 2); // c.foo(1, 2, 3);
    }
}