Coercion probably, Box<dyn BufRead> to impl BufRead but how?

I am honest with you. I am learning Rust but as IT consultant I am not able to run back to every new feature. For me, Rust is a revolutionary programming language. We all must admit it.

I cannot understand why the following code is working. I would like to understand the internals of why the call if let Ok(info) = count(file) { is working if file is a Box<dyn BufRead> but count expects impl BufRead. And how can i reach the same result of using a custom struct, say Person. Can someone explain that to me with an example and all the trait required to implement on a custom struct?

use anyhow::Result;
use std::fs::File;
use std::io::{self, BufRead, BufReader};

#[derive(Debug)]
/// Rust version of `wc`
struct Args {
    /// Input file(s)
    files: Vec<String>,

    /// Show line count
    lines: bool,

    /// Show word count
    words: bool,

    /// Show byte count
    bytes: bool,

    /// Show character count
    chars: bool,
}

#[derive(Debug, PartialEq)]
struct FileInfo {
    num_lines: usize,
    num_words: usize,
    num_bytes: usize,
    num_chars: usize,
}

// --------------------------------------------------
fn main() {
    if let Err(e) = run(Args {
        files: vec![String::from("file1.txt"), String::from("file2.txt")],
        lines: true,
        words: false,
        bytes: true,
        chars: false,
    }) {
        eprintln!("{e}");
        std::process::exit(1);
    }
}

// --------------------------------------------------
fn run(mut args: Args) -> Result<()> {
    if [args.words, args.bytes, args.chars, args.lines]
        .iter()
        .all(|v| v == &false)
    {
        args.lines = true;
        args.words = true;
        args.bytes = true;
    }

    let mut total_lines = 0;
    let mut total_words = 0;
    let mut total_bytes = 0;
    let mut total_chars = 0;

    for filename in &args.files {
        match open(filename) {
            Err(err) => eprintln!("{filename}: {err}"),
            Ok(file) => {
                if let Ok(info) = count(file) {
                    println!(
                        "{}{}{}{}{}",
                        format_field(info.num_lines, args.lines),
                        format_field(info.num_words, args.words),
                        format_field(info.num_bytes, args.bytes),
                        format_field(info.num_chars, args.chars),
                        if filename == "-" {
                            "".to_string()
                        } else {
                            format!(" {filename}")
                        },
                    );

                    total_lines += info.num_lines;
                    total_words += info.num_words;
                    total_bytes += info.num_bytes;
                    total_chars += info.num_chars;
                }
            }
        }
    }

    if args.files.len() > 1 {
        println!(
            "{}{}{}{} total",
            format_field(total_lines, args.lines),
            format_field(total_words, args.words),
            format_field(total_bytes, args.bytes),
            format_field(total_chars, args.chars)
        );
    }

    Ok(())
}

// --------------------------------------------------
fn open(filename: &str) -> Result<Box<dyn BufRead>> {
    match filename {
        "-" => Ok(Box::new(BufReader::new(io::stdin()))),
        _ => Ok(Box::new(BufReader::new(File::open(filename)?))),
    }
}

// --------------------------------------------------
fn format_field(value: usize, show: bool) -> String {
    if show {
        format!("{value:>8}")
    } else {
        "".to_string()
    }
}

// --------------------------------------------------
fn count(mut file: impl BufRead) -> Result<FileInfo> {
    let mut num_lines = 0;
    let mut num_words = 0;
    let mut num_bytes = 0;
    let mut num_chars = 0;
    let mut line = String::new();

    loop {
        let line_bytes = file.read_line(&mut line)?;
        if line_bytes == 0 {
            break;
        }
        num_bytes += line_bytes;
        num_lines += 1;
        num_words += line.split_whitespace().count();
        num_chars += line.chars().count();
        line.clear();
    }

    Ok(FileInfo {
        num_lines,
        num_words,
        num_bytes,
        num_chars,
    })
}

The type dyn BufRead implements the portions of BufRead that don't have a Self: Sized constraint, and there's a blanket implementation of BufRead for Box<B> whenever the type B implements BufRead. Taken together, that means that Box<dyn BufRead> also implements BufRead.

Because Box<T> is marked as fundamental, you can implement your own traits for it:

trait Foo {}

impl<F> Foo for Box<F> where F: Foo + ?Sized {}
1 Like

These are roughly the same:

fn count(mut file: impl BufRead) -> Result<FileInfo> { ... }
fn count<F: BufRead>(mut file: F) -> Result<FileInfo> { ... }

The F is a generic parameter, which means you can pass in any type F that meets the bounds -- any type F that implements BufRead. There's an implementation for Box<B> here, that just forwards the implementation to the boxed B when B: ?Sized + BufRead. The type dyn BufRead implements the trait BufRead, so everything works out.

Something like this, I suppose?

struct Person {
   name: String,
}

trait TellName {
    fn name(&self) -> &str;
}

impl<T: ?Sized + TellName> TellName for Box<T> {
    fn name(&self) -> &str {
        (**self).name()
    }
}

impl<T: ?Sized + TellName> TellName for &T {
    fn name(&self) -> &str {
        (**self).name()
    }
}

impl<T: ?Sized + TellName> TellName for &mut T {
    fn name(&self) -> &str {
        (**self).name()
    }
}

impl TellName for Person {
    fn name(&self) -> &str {
        &self.name
    }
}
1 Like