在 Rust 匹配表达式中保持 DRY
Keeping DRY in rust match expressions
作为一个简化的独立示例,假设我正在解析一个充满形状定义的输入文件:
// 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);
},
}
现在,假设我有 10 种或 15 种形状,而不是 3 种形状。所以我不想在每个分支中重复相同的 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/whatever 而不违反类型检查器的约束。有可能做这种动态的事情吗?这是个好主意吗?我很想在这里了解一些好的模式。
谢谢!
好的,我会尽量回答你的问题:
[is it] possible to avoid repeating the "parse-then-store" logic in every branch
答案是肯定的,但是你需要抽象出独特的部分并提取出共同的部分。我稍微改变了你的问题以获得一个更简单的例子。在这里,我们仅根据形状类型解析单个整数。
我们创建了一个包含 "change a u32
into some type and then keep a list of them" 概念的新结构 Foo
。为此,我们引入了两个通用部分 - T
,我们持有的东西的类型,以及 F
,一种将 u32
转换为该类型的方法。
为了获得一些灵活性,我还创建并实现了一个特征 ShapeMatcher
。这允许我们以通用方式获得对 Foo
的特定实例的引用 - 特征对象 。如果您不需要它,您可以将特征内联回 Foo
并将 match_it
调用内联到 if
的分支中。这在 .
中有进一步描述
#[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);
}
避免样板代码的另一种选择是某种宏驱动的嵌入式领域特定语言 (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),
}
};
}
作为一个简化的独立示例,假设我正在解析一个充满形状定义的输入文件:
// 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);
},
}
现在,假设我有 10 种或 15 种形状,而不是 3 种形状。所以我不想在每个分支中重复相同的 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/whatever 而不违反类型检查器的约束。有可能做这种动态的事情吗?这是个好主意吗?我很想在这里了解一些好的模式。
谢谢!
好的,我会尽量回答你的问题:
[is it] possible to avoid repeating the "parse-then-store" logic in every branch
答案是肯定的,但是你需要抽象出独特的部分并提取出共同的部分。我稍微改变了你的问题以获得一个更简单的例子。在这里,我们仅根据形状类型解析单个整数。
我们创建了一个包含 "change a u32
into some type and then keep a list of them" 概念的新结构 Foo
。为此,我们引入了两个通用部分 - T
,我们持有的东西的类型,以及 F
,一种将 u32
转换为该类型的方法。
为了获得一些灵活性,我还创建并实现了一个特征 ShapeMatcher
。这允许我们以通用方式获得对 Foo
的特定实例的引用 - 特征对象 。如果您不需要它,您可以将特征内联回 Foo
并将 match_it
调用内联到 if
的分支中。这在
#[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);
}
避免样板代码的另一种选择是某种宏驱动的嵌入式领域特定语言 (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),
}
};
}