Description
Here's a strawman proposal based on an idea by @leafpetersen.
Dart allows you to add behavior to classes in a compositional-ish way using mixins and with
clauses. In #4359, I suggest extending that so that you can add behavior which is applied after the class body.
Here, I suggest a different generalization: we let you do something similar for function declarations. This is sort of like decorators in Python. When combined with augmentations, it gives you much of the ability of augmented()
but (hopefully) in a more elegant way.
After all, Dart tries to place object-oriented programming and functional programming on equal footing, so why should classes have all the fun when it comes to compositionality?
Function mixin
A function mixin looks like a top-level function declaration preceded by the mixin
keyword:
mixin void profile() {
var stopwatch = Stopwatch()..start();
super();
print(stopwatch.elapsed);
}
Applying function mixins
A function mixin can't be called directly. Instead, it must be mixed into some other function declaration using a with
clause. The clause can appear after the function body:
void someSlowFunction() {
// Do something worth profiling here...
} with profile;
Multiple function mixins can be applied with a single clause:
void someFunction() {
// ...
} with someMixin, another, aThirdOne;
A function declaration can also apply mixins before its own body:
void someFunction() with someMixin, another, aThirdOne {
// ...
}
You can also apply function mixins to functions with =>
bodies:
void someFunction() with before => body with after;
(I admit that's not what I'd call beautiful. We can debate whether the body should have ;
before the trailing with
or not.)
Calling a function with mixins
Function mixins and the function's own body are linearized in the order that they appear. The function's own body is treated like an in-place function mixin declaration.
When the function is called, execution starts at the last mixin in the chain. If that mixin contains a super(...)
call, then that calls the next mixin in the chain, and so on. For example:
mixin void before1() {
print('before 1');
}
mixin void before2() {
super();
print('before 2');
}
mixin void f() with before1, before2 {
super();
print('f()');
} with after1, after2;
mixin void after1() {
super();
print('after 1');
}
mixin void after2() {
super();
print('after 2');
}
This prints:
before 1
before 2
f()
after 1
after 2
A mixin may choose to not call super(...)
, call it multiple times, or call it conditionally. The signature of super(...)
is the same as the surrounding function. A mixin may call super(...)
with different arguments than the mixin was itself passed.
In other words, it's like a function has a list of function bodies, with each one determining when the previous one gets called.
It's a compile-time error if:
-
The function mixins in a chain don't all have the same parameter signature.
-
The first function mixin (which may be the implicit one from the function's own body) contains a
super()
call since there is no preceding function mixin to call.
Function augmentations
We extend function augmentations to allow a function augmentation to add function mixins before or after the function body. If multiple augmentations add to the same function, then the mixin clauses are added in augmentation application order.
This gives augmentations a way to add code before or after a function, or both. We can probably get rid of the restrictions around "incomplete" and "complete" functions and instead treat augmenting a complete with another function body as a convenience syntax for appending a function mixin of that body.
Ambiguity
The syntax super(...)
is already valid syntax inside an instance method for a call to the superclass's call
method. This means that applying function mixins to instance methods in a class is ambiguous. Does the super(...)
call call the parent function mixin, or the superclass's call
method?
Since the latter is rare and can already be expressed using super.call(...)
, we make a language versioned change and unconditionally interpret super(...)
to mean "call the preceding function mixin".
(Tangent: If you are all-in on object-orientation, you might think of a function declaration as syntactic sugar for declaring a singleton instance of an anonymous class with a call()
method whose body is the function body. Function mixins would then be actual class mixins on that hidden class. In that case, using super()
does make sense to call the previous function mixin because it really is calling super.call()
on the mixin superclass.)