Thread overview
D’s delegates — The good, the bad, and the ugly
Jun 16, 2023
Quirin Schroll
Jun 17, 2023
Paul Backus
Jun 17, 2023
Timon Gehr
Jun 20, 2023
Quirin Schroll
Jun 17, 2023
claptrap
Jun 29, 2023
Atila Neves
Jun 29, 2023
Timon Gehr
June 16, 2023

First and foremost, D’s delegates are a great idea. I worked with C++’s member function pointers; they’re awful in syntax and the concept is fine, delegates are just better.

D’s delegates have a few issues, some are outright bugs and others are improvements that I wonder why they’re not in the language.

Some delegates must not be called

It’s weird, but it’s true.

One issue is that const and immutable currently don’t extend to the delegate’s context. This is a bug when it comes to the intention behind const and immutable being transitive:

const(void delegate()) dg = &obj.method;

This dg should not be callable: Its context may not be changed through a reference obtained by dg (as it is declared a const variable), but there’s no guarantee that dg won’t do that: method need not be annotated const.

const(void delegate() const) dg = &obj.constMethod;

This dg has a const annotation. It promises not to mutate its context; therefore, it can be called. If constMethod is not annotated const (or immutable), the assignment won’t work.

Some valid and useful conversions are rejected

Because a delegate is a tightly bound context–function pair, a delegate annotated immutable should implicitly convert to a delegate annotated mutable: The re-annotation does not change the fact that the context cannot change (by calling the delegate – because the called function simply does not do it – or by other means) and a reassignment of the delegate annotated mutable does not change that either because the context and the function pointer cannot be assigned individually.

Another conversion that should Just Work is function pointer to delegate, in fact, because a function pointer has no context, its context is immutable:

void f() @safe { writeln("Hello"); }

void delegate() @safe immutable dg = &f; // today: error

// Workaround:
void delegate() @safe immutable dg = () @trusted {
    void delegate() @safe immutable result = null;
    result.funcptr = cast(typeof(result.funcptr)) &f;
    return result;
}();
dg(); // prints "Hello" (as it should)

We have the following sequence:
void function()void delegate() immutablevoid delegate() constvoid delegate()

The first is a value conversion, the others are reference conversions.
The first conversion works when a lambda is used directly, but not when the lambda is assigned to an auto variable and then passed as an argument to a delegate-type parameter.

Inference of context qualifiers

This applies to closures. (For address of an object–method pair, the method tells the precise qualifiers.) A closure has a delegate or function pointer type, and arguably, it should have the type with the most guarantees (that’s why it infers attributes, for example). But for some reason, closures don’t infer type qualifiers.

int x;
auto dg = () => x;
pragma(msg, typeof(dg)); // int delegate() pure nothrow @nogc @safe

The type isn’t wrong, it’s just lacking: It lacks const, since x is captured by the delegate and the delegate doesn’t mutate x when it runs. So why isn’t const one of its attributes? We can ask for const explicitly, though:

int x;
auto dg = () const => x;
pragma(msg, typeof(dg)); // int delegate() const pure nothrow @nogc @safe

Does it do what it promises? No:

int x;
auto dg = () const => x += 1; // Why can I do this??

Note that dg is pure. A 0-parameter const pure delegate cannot affect values:

int x = 0;
assert(x == 0); // passes
auto dg = () const => x += 1; // Why can I do this??
pragma(msg, typeof(dg)); // int delegate() const pure nothrow @nogc @safe
dg();
assert(x == 1); // passes, but could fail due to optimizations

What about immutable?

immutable int x;
auto dg = () => x;
pragma(msg, typeof(dg)); // immutable(int) delegate() pure nothrow @nogc @safe

The immutable(int) return type sticks out, but is not the issue of concern. The interesting part is that all the things (that is, x) that dg captures are immutable. We can ask for immutable explicitly:

immutable int x;
auto dg = () immutable => x;
pragma(msg, typeof(dg)); // immutable(int) delegate() immutable pure nothrow @nogc @safe

The inference of function instead of delegate and the inference of immutable only make sense if those guarantees can be forgotten implicitly.

Some types cannot be expressed

With the type constructor attributes, one can express that the underlying function of a delegate must not mutate its context or that the context is outright immutable.

With a type constructor applied to whole delegate type, it can (rather: should in some cases) become unusable, which is bad.

What if I want to express that the delegate should not be re-assigned? That would mean: The function pointer is const or immutable (doesn’t really matter), but the context is whatever it is. I know it’s not that useful, but it’s not nothing.

If we imagine a delegate dg as a pair (dg.ptr, dg.funcptr), it would be as if dg.funcptr were const, but we leave dg.ptr (the context) alone. A non-assignable component makes a pair non-assignable. Done. Easy. Only the language has no concept for it and no syntax either. If you wonder, the syntax could be of the shape int delegate const(), where const(int delegate const()) is the same as const(int delegate()), just like const(const(int)[]) is the same type as const(int[]).

June 17, 2023

On Friday, 16 June 2023 at 15:29:47 UTC, Quirin Schroll wrote:

>

D’s delegates have a few issues, some are outright bugs and others are improvements that I wonder why they’re not in the language.

Excellent writeup. It's nice to have all of these in one place.

>

Another conversion that should Just Work is function pointer to delegate, in fact, because a function pointer has no context, its context is immutable:

I believe the reason this one doesn't work is that functions and delegates have different calling conventions. In order to allow a function pointer to convert to a delegate, the compiler would have to generate a trampoline.

June 17, 2023

On Friday, 16 June 2023 at 15:29:47 UTC, Quirin Schroll wrote:

>

First and foremost, D’s delegates are a great idea. I worked with C++’s member function pointers; they’re awful in syntax and the concept is fine, delegates are just better.

Somewhat of an aside but every time people talk about function signatures i cant help having a mental image of 20 people trying to squeeze through a single doorway. There's so much to express on function signatures it feels like 90% of the complexity of the language is right there on the entry point to a function. constness, safe, system, nogcn, nothrow, lifetimes, refness, auto this or that, return on parameters, or after the function. then you add this with delegates and how they relate to what they are being passed to or called with.

https://www.youtube.com/watch?v=M68GeL8PafE

June 18, 2023
On 6/17/23 20:37, Paul Backus wrote:
> On Friday, 16 June 2023 at 15:29:47 UTC, Quirin Schroll wrote:
>> D’s delegates have a few issues, some are outright bugs and others are improvements that I wonder why they’re not in the language.
> 
> Excellent writeup. It's nice to have all of these in one place.
> ...

Regarding context qualifiers, there is also this issue:
https://issues.dlang.org/show_bug.cgi?id=20517

>> Another conversion that should Just Work is function pointer to delegate, in fact, because a function pointer has no context, its context is `immutable`:
> 
> I believe the reason this one doesn't work is that functions and delegates have different calling conventions. In order to allow a function pointer to convert to a delegate, the compiler would have to generate a trampoline.

There's `std.functional.toDelegate` that generates the trampoline:
https://dlang.org/phobos/std_functional.html#toDelegate

However, it generates an unqualified context:

```d
import std.functional;

void foo()@safe{}

void main(){
    void delegate()immutable dg=toDelegate(&foo); // error
}
```

(Which it kind of has to because DMD incorrectly rejects the conversion from immutable to unqualified context.)
June 20, 2023

On Saturday, 17 June 2023 at 18:37:48 UTC, Paul Backus wrote:

>

On Friday, 16 June 2023 at 15:29:47 UTC, Quirin Schroll wrote:

>

Another conversion that should Just Work is function pointer to delegate, in fact, because a function pointer has no context, its context is immutable:

I believe the reason this one doesn't work is that functions and delegates have different calling conventions. In order to allow a function pointer to convert to a delegate, the compiler would have to generate a trampoline.

That’s probably the case, but it’s not a good reason. A function taking a long isn’t called the same as a function taking an int (different sizes), still I can call that function with an int; the value can be converted. Having to do an explicit conversion would be annoying and serve no purpose. The same is true for functiondelegate. And there isn’t even an explicit conversion the likes of cast(DelegateType) fp.

June 29, 2023

On Friday, 16 June 2023 at 15:29:47 UTC, Quirin Schroll wrote:

>

First and foremost, D’s delegates are a great idea. I worked with C++’s member function pointers; they’re awful in syntax and the concept is fine, delegates are just better.

[...]

Are there issues in bugzilla for these?

June 29, 2023
On 6/29/23 16:52, Atila Neves wrote:
> On Friday, 16 June 2023 at 15:29:47 UTC, Quirin Schroll wrote:
>> First and foremost, D’s delegates are a great idea. I worked with C++’s member function pointers; they’re awful in syntax and the concept is fine, delegates are just better.
>>
>> [...]
> 
> Are there issues in bugzilla for these?

Yes, e.g.:
https://issues.dlang.org/show_bug.cgi?id=9149

Though there are also other somewhat egregious issues related to delegates, e.g.:
https://issues.dlang.org/show_bug.cgi?id=23136
https://issues.dlang.org/show_bug.cgi?id=22135