Skip to main content
v0.13.0

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.