Description
One common programing pattern is to have a reusable utility which wraps a function to perform logic before/after and to transform the result.
A typical example is a logger middleware. One might write:
T Function() logged<T>(T Function() cb) {
return () {
print('Before');
final value = cb();
print('After $value');
return value;
};
}
Then used as:
class Example {
late final method = logged(() {
return 42;
});
}
The problem is:
Our logged
util is bound to the T Function()
prototype. It does not accept any other Function variants.
If we want a Example.method
which takes parameters, we have to write a new logged
; such as loggedUnary
/ loggedBinary
/ ...
This is a common issue. For example we have Zone.current.bindCallback
vs Zone.current.bindUnaryCallback
vs Zone.current.bindBinaryCallback
...
Proposal: Make all Functions have a compose
method, and add a TypedFunction<Return, Arg>
interface
The idea is that all functions would implement a new interface defined as:
// A new primitive, similar to Invocation but typed.
abstract class ArgumentList {}
abstract class TypedFunction<ReturnT, ArgsT extends ArgumentList> {
TypedFunction<NewReturnT, ArgsT> compose<NewReturnT>(
NewReturnT Function(
ReturnT Function(ArgsT args),
ArgT args,
) cb,
);
// Not variadic arguments. I'm just using ... because I don't know what other syntax would fit
ReturnT call(...ArgsT args);
}
Consider:
String nullary() => 0;
int unary(int a) => a;
int complex(int a, {required String b, double c = 42, bool? d}) => 42;
Those functions would implement TypedFunction
as followed:
TypedFunction<String, _()> a = nullary;
TypedFunction<int, _(int)> b = unary;
TypedFunction<int, _(int, {required String b, double c = 42, bool? d})> b = complex;
Note:
The second generic type in this snippet isn't a Record! It's a new type that represents a subclass of ArgumentList
.
Based on those interfaces, we can do:
final complex2 = complex.compose((call, args) {
print('Before $args');
final result = call(args);
print('After $result');
return result;
})
complex2(1, b: 'b', c: 2, d: false);
This would log:
Before _(1, b: 'b', c: 2, d: false)
After 42
Example 1: Logging
Using this new feature, we could implement an all purpose logged
utility:
TypedFunction<ResT, ArgsT> logged<ResT, ArgsT extends ArgumentList>(
TypedFunction<RestT, ArgsT> cb,
) {
return cb.compose((call, args) {
print('Before $args');
final result = call(args);
print('After $result');
return result;
});
}
This could then be used as
class Example {
late final method = logged(() {
return 42;
});
late final method2 = logged((int arg, {required String value}) {
...
}
}
void main() {
final example = Example();
example.method();
example.method2(1, value: 'value');
}
Example 2: Wrapping functions in a Result<T>
Consider:
sealed class Result<T> {}
class ResultData<T> implements Result<T> {}
class ResultError<T> implements Result<T> {}
We could implement a generic guard
utility:
TypedFunction<Result<T>, ArgsT> guard<T, ArgsT extends ArgumentList>(
TypedFunction<T, ArgsT> cb,
) {
return cb.compose((call, args) {
try {
final result = call(args);
return ResultData(result);
} catch (err) {
return ResultError(result);
}
});
}
This could then be used as
class Example {
late final method = guard(() {
return 42;
});
late final method2 = guard((int arg, {required String value}) {
return 42;
}
}
void main() {
final example = Example();
Result<int> a = example.method();
Result<int> b = example.method2(1, value: 'value');
}