visitor
A library that generates code matching the visitor pattern based on annotations made on classes and typedefs.
For example, the following definition in a func.dart file
import 'package:visitor/visitor.dart';
part 'func.g.dart';
@Visitor(name: "Func")
class _Func<R> {}
@VisitorBranch(of: _Func)
typedef _Func$Supplier<T, R> = T Function();
@VisitorBranch(of: _Func)
typedef _Func$UnaryOperator<T, R> = T Function(R x);
@VisitorBranch(of: _Func)
typedef _Func$BinaryOperator<T, R> = T Function(R x, R y);
generates the following code in a func.g.dart file:
abstract class Func<R> {
const factory Func.supplier() = _OnSupplierFunc;
const factory Func.unaryOperator(R x) = _OnUnaryOperatorFunc;
const factory Func.binaryOperator(R x, R y) = _OnBinaryOperatorFunc;
const Func._();
T choose<T>({
required _Func$Supplier<T, R> onSupplier,
required _Func$UnaryOperator<T, R> onUnaryOperator,
required _Func$BinaryOperator<T, R> onBinaryOperator,
});
}
class _OnSupplierFunc<R> extends Func<R> {
const _OnSupplierFunc() : super._();
@override
T choose<T>({
required _Func$Supplier<T, R> onSupplier,
required _Func$UnaryOperator<T, R> onUnaryOperator,
required _Func$BinaryOperator<T, R> onBinaryOperator,
}) {
return onSupplier();
}
}
class _OnUnaryOperatorFunc<R> extends Func<R> {
final R x;
const _OnUnaryOperatorFunc(this.x) : super._();
@override
T choose<T>({
required _Func$Supplier<T, R> onSupplier,
required _Func$UnaryOperator<T, R> onUnaryOperator,
required _Func$BinaryOperator<T, R> onBinaryOperator,
}) {
return onUnaryOperator(x);
}
}
class _OnBinaryOperatorFunc<R> extends Func<R> {
final R x;
final R y;
const _OnBinaryOperatorFunc(this.x, this.y) : super._();
@override
T choose<T>({
required _Func$Supplier<T, R> onSupplier,
required _Func$UnaryOperator<T, R> onUnaryOperator,
required _Func$BinaryOperator<T, R> onBinaryOperator,
}) {
return onBinaryOperator(x, y);
}
}
You can now use the generated code:
int apply(Func<int> func) {
final int result = func.choose<int>(
onSupplier: () => 1,
onUnaryOperator: (x) => 2 * x,
onBinaryOperator: (x, y) => x + y,
);
return result;
}
void main() {
Func<int> f;
f = Func.supplier();
print(apply(f));
// reaches `onSupplier: () => 1`, outputs 1
f = Func.unaryOperator(2);
print(apply(f));
// reaches `onUnaryOperator: (x) => 2 * x`, outputs 4
f = Func.binaryOperator(3, 5);
print(apply(f));
// reaches `onBinaryOperator: (x, y) => x + y`, outputs 8
}
Alternatively, you can think of it as a similar concept of Kotlin's sealed classes.
Features
- Handles type parameters (as seen in the example above).
- Handles recursive parameter types.
- Handles named and optional parameters.
Getting started
Add this dependency to your pubspec.yaml as a dev dependency.
Usage
This library exports two annotations: Visitor
and VisitorBranch
.
The Visitor
annotation is meant to be used in classes. The class name must
be prefixed with one or more underscores (e.g. _Example
).
@Visitor()
class _Example {}
The VisitorBranch
annotation is meant to be used in type definitions (typedef
s).
It receives one required named parameter of
, which should be its Visitor type.
@VisitorBranch(of: _Example)
Each typedef annotated with VisitorBranch
will become a branch of its respective visitor, and
- must alias a function with zero or more parameters;
- must have a parameter type
T
, which the aliased function must return; - can have any name, but as a convention, it should start with
_$
to avoid external referencing. If more than one visitor is being declared in a single file, to avoid name clashing, its name should be_
+ the camel-cased name of its visitor +$
+ the camel-cased name of the branch.
@VisitorBranch(of: _Example)
typedef _$FirstBranch<T> = T Function(int);
Below the imports of the current file, add a part statement. E.g. if the name of the file is file.dart:
part 'file.g.dart';
After adding the Visitor
, its VisitorBranch
es and the part statement, run the following command
in the root directory of your project:
dart run build_runner build
# Alternatively, if you're using Flutter
flutter pub run build_runner build
The generated code will be added in the file described in the part statement. The following changes will be applied:
- the name of the generated visitor will be the class name without the first underscore (e.g.
Example
). - the names of the generated factory methods in the visitor will be each branch name as camel-case
(e.g.
firstBranch
). - the names of the generated subclasses of the visitor will be each branch name as Pascal-case,
prefixed with
_On
and suffixed with the Pascal-cased visitor name (e.g._OnFirstBranchExample
). - the names of the callbacks of the visitor will be each branch name as Pascal-case, prefixed with
on
(e.g.onFirstBranch
).
abstract class Example implements _Example {
const factory Example.firstBranch(int p0) = _OnFirstBranchExample;
const Example._();
T choose<T>({
required T Function(int) onFirstBranch,
});
}
class _OnFirstBranchExample extends Example {
final int p0;
const _OnFirstBranchExample(int p0) : super._();
T choose<T>({
required T Function(int) onFirstBranch,
}) {
return onFirstBranch(p0);
}
}
On typedefs annotated with VisitorBranch
, the linter may produce an unused element
warning. To avoid that, you can add a
// ignore_for_file: unused_element
to the top of the file.
Examples
You can see the usage in the example/lib/visitor_example.dart file.
In the example/lib/examples,
- the expression.dart shows usage on recursive parameter types;
- the diff_change.dart shows usage on named parameters;
- the loading_state.dart shows usage on type parameters.