Sum types are very tempting. I often find myself wanting to define a new type because they’re cool! However, there are numerous cases where a custom enum can elide use of built-ins.

Consider a program that can either read from a file or standard in. Based on the previous sentence alone, it sounds like we have defined a new type with two variants: File and Stdin:

enum FileType<'a> {
    File(&'a str),
    Stdin,
}

Using our new enum we can write a function that opens a FileType and prints its lines.

fn print(file: FileType) -> io::Result<()> {
    let stdin = stdin();
    match file {
        FileType::File(f) => {
            let file = File::open(f)?;
            print_lines(&mut BufReader::new(file))
        }
        FileType::Stdin => print_lines(&mut stdin.lock()),
    }
}

fn print_lines<T: BufRead>(buf: T) -> io::Result<()> {
    for line in buf.lines() {
        println!("{}", line?);
    }

    Ok(())
}

This works ok, but it forces us to use exhaustive case analysis on our FileType enum. Let’s try to rescope this problem with Option<&str>

fn print(file: Option<&str>) -> io::Result<()> {
    let stdin = stdin();
    file.map_or_else(
        || print_lines(&mut stdin.lock()),
        |f| File::open(f).and_then(|file| print_lines(&mut BufReader::new(file))),
    )
}

Using Option<&str> gets us all of the combinators defined on Option<T> to write the same function concisely.

So is this really better? I don’t know. It might be.