8. Pluggable output
The pipeline produces report lines. This page makes where they go swappable without touching anything upstream.
An aspect
An aspect declares a capability — a set of methods a type can provide. extend
attaches it to a type:
aspect Sink {
fun emit(&self, line: String);
}
struct Console { }
extend Console: Sink {
fun emit(&self, line: String) { println(line); }
}
struct Prefixed { tag: String }
extend Prefixed: Sink {
fun emit(&self, line: String) { println("[" + self.tag + "] " + line); }
}
fun main() {
let a := Console { };
let b := Prefixed { tag = "metrics" };
a.emit("user 7: 51");
b.emit("user 7: 51");
}
user 7: 51
[metrics] user 7: 51
Generic over any sink
A function that only needs "something that can emit" takes a bounded type
parameter:
aspect Sink { fun emit(&self, line: String); }
struct Console { }
extend Console: Sink { fun emit(&self, line: String) { println(line); } }
fun report<S: Sink>(sink: &S, lines: List<String>) {
for (l in lines.as_slice()) { sink.emit(l); }
}
fun main() {
var lines: List<String> := List::new();
lines.push("user 7: 51");
lines.push("user 3: 2");
let out := Console { };
report(&out, lines);
}
user 7: 51
user 3: 2
A collection of different sinks
dyn Sink is a single type that any Sink implementer coerces to — so a
List<dyn Sink> can hold a Console and a Prefixed at once, and you call emit
on each without knowing which is which:
aspect Sink { fun emit(&self, line: String); }
struct Console { }
extend Console: Sink { fun emit(&self, line: String) { println(line); } }
struct Prefixed { tag: String }
extend Prefixed: Sink { fun emit(&self, line: String) { println("[" + self.tag + "] " + line); } }
fun main() {
var sinks: List<dyn Sink> := List::new();
sinks.push(Console { });
sinks.push(Prefixed { tag = "metrics" });
for (s in sinks.as_slice()) {
s.emit("(report complete)");
}
}
(report complete)
[metrics] (report complete)
Ruling a capability out
A bound can be negative. Suppose a blanket rule makes every Wrapper<T>
Loggable. extend<T> Secret<T>: !Loggable says a Secret<T> is never
Loggable — the absence is a claim the type makes on purpose, and it wins over the
blanket:
aspect Loggable;
struct Wrapper<T> { value: T }
extend<T> Wrapper<T>: Loggable; // blanket: every Wrapper is Loggable
struct Secret<T> { value: T }
extend<T> Secret<T>: !Loggable; // ...except Secret, which opts out
fun log_it<T: Loggable>(x: T) { }
fun main() {
log_it(Wrapper { value = 42 }); // Wrapper is Loggable — fine
// log_it(Secret { value = 42 }); // rejected: Secret is !Loggable
println("ok");
}
ok
By the end you'll have
A Sink aspect with two implementations, a generic report, and a List<dyn Sink>
that fans one line out to several destinations.
Next: Reusable, and split into files — make the aggregator generic and break the project up.