Bosh
Bosh

Reputation: 8738

Keeping DRY in rust match expressions

As a simplified, self-contained example, let's say I'm parsing an input file full of shape definitions:

// shapes.txt
Circle: radius 1, color blue
Square: edge 5, color red
Triangle: edge 2 , color black
Triangle: edge 2 , color white

I want to parse these into structs like:

struct Circle {
    radius: i32,
    color: String
}

struct Square {
    edge: i32,
    color: String
}

struct Triangle {
    edge: i32,
    color: String
}

I'd like to parse these into a set of shape-specific vectors like:

CircleDb: Vec<Circle>;
TriangleDb: Vec<Triangle>;
SquareDb: Vec<Square>;

... using a match block like:

match inputFile.nextWord() {
    "Circle" => {
        Circle c = parseCircle(inputFile);
        CircleDb.push(c);
    },
    "Square" => {
        Square s = parseSquare(inputFile);
        SquareDb.push(s);
    },
    "Triangle" => {
        Triangle t = parseTriangle(inputFile);
        TriangleDb.push(t);
    },
}

Now, imagine that instead of 3 kinds of shapes, I've got 10 or 15. So I don't want to repeat the same sequence of x=parseX(inputFile); XDb.push(x); within each branch. I'd rather say something like:

let myMatcher = match inputFile.nextWord() {
    "Circle" => CircleMatcher,
    "Square" => SquareMatcher,
    "Triangle" => TriangleMatcher,
};
myMatcher.store(myMatcher.parse(inputFile));

But I can't figure out any consistent way to define a Matcher struct/type/trait/whatever without violating constraints of the type checker. Is it possible to do this kind of dynamic thing? Is it a good idea? I'd love to get a sense of some good patterns here.

Thanks!

Upvotes: 0

Views: 432

Answers (2)

Shepmaster
Shepmaster

Reputation: 430574

Ok, I'll try to answer your question:

[is it] possible to avoid repeating the "parse-then-store" logic in every branch

The answer is yes, but you will need to abstract out the parts that are unique and extract out the parts that are common. I changed your problem a bit to have an easier example. Here, we parse just a single integer, based on what shape type it is.

We create a new struct Foo that holds the concept of "change a u32 into some type and then keep a list of them". To do that, we introduce two generic pieces - T, the type of thing we are holding, and F, a way of converting a u32 into that type.

To allow for some flexibility, I also created and implemented a trait ShapeMatcher. This allows us to get a reference to a specific instance of Foo in a generic way - a trait object. If you don't need that, you could just inline the trait back into Foo and also inline the match_it call into the branches of the if. This is further described in Returning and using a generic type with match.

#[derive(Debug)]
struct Circle(u32);
#[derive(Debug)]
struct Square(u32);

struct Foo<T, F> {
    db: Vec<T>,
    matcher: F,
}

impl<T, F> Foo<T, F>
    where F: Fn(u32) -> T
{
    fn new(f: F) -> Foo<T, F> { Foo { db: Vec::new(), matcher: f } }
}

trait ShapeMatcher {
    fn match_it(&mut self, v: u32);
}

impl<T, F> ShapeMatcher for Foo<T, F>
    where F: Fn(u32) -> T
{
    fn match_it(&mut self, v: u32) {
        let x = (self.matcher)(v);
        self.db.push(x);
    }
}

fn main() {
    let mut circle_matcher = Foo::new(Circle);
    let mut square_matcher = Foo::new(Square);

    for &(shape, value) in &[("circle", 5),("circle", 42),("square", 9)] { 
        let matcher: &mut ShapeMatcher =
            if shape == "circle" { &mut circle_matcher }
            else                 { &mut square_matcher };

        matcher.match_it(value);
    }

    println!("{:?}", circle_matcher.db);
    println!("{:?}", square_matcher.db);
}

Upvotes: 1

swizard
swizard

Reputation: 2701

Another option for avoiding boilerplate code would be some kind of macro-powered embedded domain specific language (eDSL). It is not always the best idea (especially in Rust), but sometimes this method is more expressive for tasks like yours. For example, consider a syntax:

    shapes_parse! { 
        inspecting line; { 
            Circle into circle_db,
            Square into square_db,
            Triangle into triangle_db
        }
    }

which expands in the following code:

    match line[0] {
        "Circle" => { circle_db.push(Circle::parse(&line[1..])); },
        "Square" => { square_db.push(Square::parse(&line[1..])); },
        "Triangle" => { triangle_db.push(Triangle::parse(&line[1..])); },
        other => panic!("Unexpected type: {}", other),
    }

using this macro:

macro_rules! shapes_parse {
    ( inspecting $line:expr; { $($name:ident into $db:expr),* } ) => {
        match $line[0] {
            $( stringify!($name) => { $db.push($name::parse(&$line[1..])); } )+
            other => panic!("Unexpected shape: {}", other),
        }
    };
}

workining example on playpen

Upvotes: 0

Related Questions