// Copyright 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0
pub trait Logger {
/// Log a message at the given verbosity level.
fn log(&self, verbosity: u8, message: &str);
}
struct StderrLogger;
impl Logger for StderrLogger {
fn log(&self, verbosity: u8, message: &str) {
eprintln!("[stderr] verbosity={verbosity}: {message}");
}
}
struct StdoutLogger;
impl Logger for StdoutLogger {
fn log(&self, verbosity: u8, message: &str) {
println!("[stdout] verbosity={verbosity}: {message}");
}
}
/// Only log messages matching a filtering predicate.
struct Filter<L, P> {
inner: L,
predicate: P,
}
impl<L, P> Filter<L, P>
where
P: Fn(u8, &str) -> bool,
{
fn new(inner: L, predicate: P) -> Self {
Self { inner, predicate }
}
}
impl<L, P> Logger for Filter<L, P>
where
L: Logger,
P: Fn(u8, &str) -> bool,
{
fn log(&self, verbosity: u8, message: &str) {
if (self.predicate)(verbosity, message) {
self.inner.log(verbosity, message);
}
}
}
fn main() {
let prefix = "yikes";
let max_verbosity = 3;
// Logger that writes to stderr and filters based on message contents.
let logger = Filter::new(StderrLogger, |_verbosity, msg| msg.contains(prefix));
logger.log(5, "FYI");
logger.log(1, "yikes, something went wrong");
logger.log(2, "uhoh");
// Logger that writes to stdout and filters based on verbosity.
let logger =
Filter::new(StdoutLogger, |verbosity, _msg| verbosity <= max_verbosity);
logger.log(5, "FYI");
logger.log(1, "yikes, something went wrong");
logger.log(2, "uhoh");
}
Storing Closures: To store a closure in a struct, we use a generic type
parameter (here P). This is because every closure in Rust has a unique,
anonymous type generated by the compiler.
Fn Trait Bound: The bound P: Fn(u8, &str) -> bool tells the compiler
that P can be called as a function with the specified arguments and return
type. We use Fn (instead of FnMut or FnOnce) because log takes
&self, so we can only access the predicate immutably.
Calling fields: We invoke the closure using (self.predicate)(...). The
parentheses around self.predicate are necessary to disambiguate between
calling a method named predicate and calling the field itself.
Discuss why Fn is required. If we used FnMut, log would need to take
&mut self, which conflicts with the Logger trait signature. If we used
FnOnce, we could only log a single message!
The impl block for new also includes the bounds. While technically not
strictly required for the struct definition itself (bounds can be placed only
on impl blocks that use them), putting them on new helps type inference.