将DRY保持在生锈匹配表达式中

时间:2015-02-26 19:21:48

标签: polymorphism rust traits

作为一个简化的自包含示例,假设我正在解析一个充满形状定义的输入文件:

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

我想将这些解析成结构,如:

struct Circle {
    radius: i32,
    color: String
}

struct Square {
    edge: i32,
    color: String
}

struct Triangle {
    edge: i32,
    color: String
}

我想将这些解析为一组特定于形状的矢量,如:

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

...使用匹配块,如:

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);
    },
}

现在,想象一下,而不是3种形状,我有10或15.所以我不想在每个分支内重复相同的x=parseX(inputFile); XDb.push(x);序列。我宁愿说些什么:

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

但我无法找出任何一致的方法来定义Matcher struct / type / trait / what而不违反类型检查器的约束。有可能做这种动态的事吗?这是个好主意吗?我想在这里了解一些好的模式。

谢谢!

2 个答案:

答案 0 :(得分:1)

好的,我会尝试回答你的问题:

  

[是否]可以避免重复&#34; parse-then-store&#34;每个分支的逻辑

答案是肯定的,但你需要抽象出独特的部分并提取出常见的部分。我稍微改变了你的问题,以便有一个更简单的例子。在这里,我们根据它的形状类型解析一个整数。

我们创建了一个新的结构Foo,其中包含&#34;将u32更改为某种类型的概念,然后保留它们的列表&#34;。为此,我们引入了两个通用部分 - T,我们持有的内容类型,以及F,一种将u32转换为该类型的方法。

为了获得一些灵活性,我还创建并实现了一个特征ShapeMatcher。这允许我们以通用方式获取对Foo的特定实例的引用 - 特征对象。如果您不需要,可以将特征内联回Foo,并将match_it内联调用if的分支。这在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);
}

答案 1 :(得分:0)

避免样板代码的另一个选择是某种宏驱动的嵌入式域特定语言(eDSL)。它并不总是最好的主意(特别是在Rust中),但有时这种方法对于像你这样的任务更具表现力。例如,考虑一种语法:

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

扩展为以下代码:

    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),
    }

使用此宏:

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