Open DrafaKiller opened 1 year ago
This proposal brings up memories of :before
, :after
and :around
methods in CLOS (cf. this SO question on this topic), and the entire subculture known as aspect-oriented programming, and there are also some connections to design by contract. So there are lots of ideas out there about this kind of feature, and they have been refined over many years.
One obvious place to look for this kind of feature is https://pub.dev/packages/aspectd.
The main trade-off is probably: Do you want this feature to be non-intrusive? That is, should we be able to declare a 'before' or 'after' method for a method C.m
without having editing powers over the library that declares C
or any of its superclasses?
If they're allowed to be intrusive then I think it's useful to take a look at a manually written version of the same thing:
abstract class B {
void init() { // The "official" `init`, only used to do method combination.
initBefore();
doInit();
initAfter();
}
void doInit() { // The actual implementation of `init` goes here.
print('Run B.init code');
}
void initBefore(); // Could be abstract, could do nothing, whatever fits.
void initAfter();
}
class A extends B {
@override
void doInit() {
print('Run A.init code');
}
@override
void initBefore() {
print('Run code before init');
}
@override
void initAfter() {
print('Run code after init');
}
}
void main() => A().init();
The point is that this manual version shows all the questions that we might want to consider: Which parameters will we pass to the ::before
and ::after
methods? Do different declarations of ::before
override each other, or do they all get called, according to the override ordering, and similarly for ::after
? How do we combine returned results from all these methods?
One thing to note is that this kind of feature probably works best with a general approach to methods where overriding is considered to be a rare exception, and method combination is the normal and desirable semantics (a bit like constructors: You always call a generative constructor for every class in the entire superclass chain; so we'd have @mustCallSuper
all over the place).
The main trade-off is probably: Do you want this feature to be non-intrusive? That is, should we be able to declare a 'before' or 'after' method for a method C.m without having editing powers over the library that declares C or any of its superclasses?
I think it should be non-intrusive, it can be declared for any method, as that would also be a possibility with the current overriding.
If they're allowed to be intrusive then I think it's useful to take a look at a manually written version of the same thing:
There's still a problem with that manually written version, and that is when working with mixins.
When extending the method initBefore()
in multiple mixins, if a mixin doesn't call super.initBefore()
it cuts the chain.
Which parameters will we pass to the ::before and ::after methods?
I think the more intuitive, at least to me, is if the parameters are the same as the original method. That way the method can be overridden as normal, and later just adding ::before
or ::after
in front of the method name. At least this would feel like the simplest way.
The methods method()
, method::before()
and method::after()
would need to have the same parameters so they are easily chainable. In this case, method::before()
and method::after()
are strictly bound to the original method, method()
.
Then it would be up to the original method to call, internally, ::before()
, then super
, then ::after()
.
We can see it as method()
being a normal overriding method, but is surrounded with these pseudo-methods, if they exist.
How do we combine returned results from all these methods?
I think pseudo-methods shouldn't return, you may declare it with a return type, but pseudo-methods must not have a return. If declaring a pseudo-method with a return keyword, it will throw a compile or syntax error.
Allowing the developer to declare pseudo-methods with void
return type might be a future problem when implementing method overloading.
In case of Common Lisp, the return value is ignored.
When calling those methods, the return value will be ignored, as so:
int init::before() { ... }
int init::after() { ... }
// Internal
@override
int init() {
// Return vales are ignored.
init::before();
final value = super.init();
init::after();
return value;
}
If the pseudo functions cannot return anything (which is reasonable), then we should decide what happens if they, or the super method, throws.
If init::before
throws, should super.init
be called? (Probably not).
If super.init
throws, should init::after
be executed? (.... much less clear. Let's say "no" and also add init::finally
which always gets called. Well, at least if ::before
didn't throw. Should it depend on whether ::before
is declared before or after ::finally
?)
Can you override the method and provide ::before
and ::after
in the same class, or can you only either override or add before/after?
And then there are the parameters. Using the "same parameters" as the original method is under-defined, because the original method is an interface method specified by a function type, which does not include names for positional parameters. If you want to actually access the positional parameters, you need to give them names somehow. Which means repeating them, and avoiding having to do that was one of the benefits listed of having ::before
instead of just writing a wrapper method manually.
(Or we could just name them $1
, ..., $n
for you. No readability issues with that. 😁).
This reminds me of the Drop
trait in Rust, where when a variable goes out of scope, the compiler automatically calls the drop
function, and you can override it however you wish for a certain type.
Another thing this will help with: users are often confused about whether to call super.foo()
before or after their override. This makes a big difference in the case of Flutter's State
:
class MyWidget extends StatefulWidget {
String initialText;
const MyWidget(this.initialText);
@override
MyState createState() => MyState();
}
class MyState extends State<MyWidget> {
final controller = TextEditingController();
@override
void initState() {
// This won't work because `super.initState()` must be called *first*
controller.text = widget.initialText;
super.initState();
}
@override
void dispose() {
// This won't work because `super.dispose()` must be called *last*
super.dispose();
controller.dispose();
}
}
Overriding State.initState::after
and State.dispose::before
instead would be much clearer. Though I suppose this example shows there could be some value to a warning not to override something -- eg, State.initState::before
or State.dispose::after
. Maybe these should only be overrideable if the superclass explicitly declares them. Otherwise, just override the whole method and call super.foo()
yourself?
Proposal
The Pseudo-methods ::before and ::after act as methods, overriding the declared methods which they are bound to. They are a simpler alternative to avoid having to call super, or an option to organize code in your own way.
This also avoids overriding methods and forgetting to call super.
For example, forgetting to call
super.initState()
/super.dispose()
in flutter.The basic idea of it is:
method::before()
will be called beforemethod()
method::after()
will be called aftermethod()
super
must be calledMotivation
When extending a class, you might want to keep the logic from the previous implementation. But for that you must call
super.method()
, and that can be even more inconvenient if that method has a lot of arguments that you must pass in.Sometimes you might even forget to call it, and other times, it just looks out of place.
Instead we can avoid having to deal with
super
, and even completely abstracting from the fact that it's being used internally. This way, all we care about is that our code is going to run before or after the method.Example
Let's imagine we have a file class, with the following methods:
Now, we want to automatically save the file before closing.
Currently, we would have to do:
But with the use of pseudo-methods, we can do:
Why "Pseudo-methods"?
Just like in CSS, pseudo-elements ::before and ::after are extensions of an element, which will be displayed before or after the original element.
In our case, pseudo-methods might look like methods, but instead, they are an extension of a method, thus pseudo. Without the main method, the pseudo-methods wouldn't make sense.
Full Syntax Example