D’s delegates — The good, the bad, and the ugly
Quirin Schroll
qs.il.paperinik at gmail.com
Fri Jun 16 15:29:47 UTC 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:
```d
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`.
```d
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`:
```d
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() immutable` → `void
delegate() const` → `void 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.
```d
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:
```d
int x;
auto dg = () const => x;
pragma(msg, typeof(dg)); // int delegate() const pure nothrow
@nogc @safe
```
Does it do what it promises? No:
```d
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:
```d
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`?
```d
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:
```d
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[])`.
More information about the Digitalmars-d
mailing list