Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Returning and using a generic type with match

Tags:

rust

I'm working on a simple Rust app that accepts stdin and acts on the basis of it. I'd like to have each command return a vector of results.

Different commands may return differently typed vectors; the list method returns a vector of PathBufs, but the default match arm returns strings:

use std::{io, fs};
use std::path::PathBuf;

fn main() {
    let mut input = String::new();
    io::stdin().read_line(&mut input).expect("Failed to read line");
    let chars_to_trim: &[char] = &['\n'];
    let trimmed_input: &str = input.trim_matches(chars_to_trim);

    let no_match = vec!["No Match"];

    let result = match trimmed_input {
        "list" => list(),
        _ => no_match,
    };
}

fn list() -> Vec<PathBuf> {
    println!("{}", "list of lockfiles here");
    let entries = fs::read_dir("/tmp").expect("Failed to read /tmp");
    let all: Result<_, _> = entries.map(|entry| entry.map(|e| e.path())).collect();
    all.expect("Unable to read an entry")
}

This causes compilation to fail:

error[E0308]: match arms have incompatible types
  --> src/main.rs:12:22
   |
12 |         let result = match trimmed_input {
   |                      ^ expected struct `std::path::PathBuf`, found &str
   |
   = note: expected type `std::vec::Vec<std::path::PathBuf>`
   = note:    found type `std::vec::Vec<&str>`
note: match arm with an incompatible type
  --> src/main.rs:14:18
   |
14 |             _ => no_match,
   |                  ^^^^^^^^

What is the idiomatic Rust way to handle this? I've read through the documentation on generics but I'm not sure how to apply it.

like image 753
Allyl Isocyanate Avatar asked Feb 18 '15 17:02

Allyl Isocyanate


1 Answers

Here's a reduced testcase:

use std::path::PathBuf;

fn main() {
    let paths: Vec<PathBuf> = Vec::new();
    let defaults: Vec<&'static str> = Vec::new();

    let result = match 1 {
        1 => paths,
        _ => defaults,
    };
}

As the error message is trying to say, Rust requires that a single variable will always have the same type. It simply doesn't make sense to have one variable be an unknown type out of a set.

The most straight-forward thing you can do is create an enum that wraps both cases you have:

use std::path::PathBuf;

enum MyThing<'a> {
    Str(&'a str),
    Path(PathBuf),
}

fn main() {
    let paths: Vec<PathBuf> = Vec::new();
    let defaults: Vec<&'static str> = Vec::new();

    let result: Vec<_> = match 1 {
        1 => paths.into_iter().map(MyThing::Path).collect(),
        _ => defaults.into_iter().map(MyThing::Str).collect(),
    };
}

You could also just pick a middle ground and convert to that type, perhaps String:

use std::path::PathBuf;

fn main() {
    let paths: Vec<PathBuf> = Vec::new();
    let defaults: Vec<&'static str> = Vec::new();

    let result: Vec<_> = match 1 {
        1 => paths.into_iter().map(|p| p.to_string_lossy().into_owned()).collect(),
        _ => defaults.into_iter().map(|s| s.to_string()).collect(),
    };
}

A third option is to create a trait and implement it for both types. You can then create a trait object. This option is closest to dynamic languages that you may be familiar with. It adds an extra layer of indirection, allowing for more flexibility:

use std::path::PathBuf;

trait MyTrait {
    fn size(&self) -> u8;
}

impl MyTrait for PathBuf {
    fn size(&self) -> u8 {
        15
    }
}

impl<'a> MyTrait for &'a str {
    fn size(&self) -> u8 {
        88
    }
}

fn main() {
    let paths: Vec<PathBuf> = Vec::new();
    let defaults: Vec<&'static str> = Vec::new();

    let result: Vec<_> = match 1 {
        1 => paths.into_iter().map(|p| Box::new(p) as Box<MyTrait>).collect(),
        _ => defaults.into_iter().map(|s| Box::new(s) as Box<MyTrait>).collect(),
    };
}
like image 198
Shepmaster Avatar answered Nov 05 '22 13:11

Shepmaster