Description
Guard Clause
I'm taking my inspiration from Ruby for this one.
Avoid use of nested conditionals for flow of control. Prefer a guard clause when you can assert invalid data. A guard clause is a conditional statement at the top of a function that bails out as soon as it can. - Ruby Style Guide @ No Nested Conditionals
First I'll provide three examples of the wrong way to do it. These are overly nested and trying to follow the paths or guess with else belongs to what can be confusing at a glance. These examples are a simple file config parse
Wrong (A)
fn read_config1() -> Result<Config, Error> {
let file = File::open("program.cfg");
if let Ok(f) = file {
let mut buf_reader = BufReader::new(f);
let mut contents = String::new();
if buf_reader.read_to_string(&mut contents).is_ok() {
let mut data: Vec<u8> = vec![];
for item in contents.
split("\n").
map(|s| s.to_string()).
filter(|s| !s.is_empty()).
collect::<Vec<String>>() {
let num = item.parse::<u8>();
if let Ok(conf) = num {
data.push(conf);
} else {
return Err(Error::ConfigParseFail);
}
}
Ok( Config { data: data } )
} else {
Err(Error::ConfigLoadFail)
}
} else {
Err(Error::ConfigLoadFail)
}
}
Wrong (B)
fn read_config2() -> Result<Config, Error> {
let file = File::open("program.cfg");
match file {
Ok(f) => {
let mut buf_reader = BufReader::new(f);
let mut contents = String::new();
match buf_reader.read_to_string(&mut contents) {
Ok(_) => {
let mut data: Vec<u8> = vec![];
for item in contents.
split("\n").
map(|s| s.to_string()).
filter(|s| !s.is_empty()).
collect::<Vec<String>>() {
let num = item.parse::<u8>();
match num {
Ok(conf) => data.push(conf),
_ => { return Err(Error::ConfigParseFail); },
}
}
Ok( Config { data: data } )
},
_ => { Err(Error::ConfigLoadFail) }
}
},
_ => { Err(Error::ConfigLoadFail) }
}
}
Wrong (C)
fn read_config3() -> Result<Config, Error> {
let file = File::open("program.cfg");
if let Ok(f) = file {
let mut buf_reader = BufReader::new(f);
let mut contents = String::new();
if buf_reader.read_to_string(&mut contents).is_ok() {
let mut data: Vec<u8> = vec![];
for item in contents.
split("\n").
map(|s| s.to_string()).
filter(|s| !s.is_empty()).
collect::<Vec<String>>() {
let num = item.parse::<u8>();
if let Ok(conf) = num {
data.push(conf);
} else {
return Err(Error::ConfigParseFail);
}
}
return Ok( Config { data: data } );
}
}
Err(Error::ConfigLoadFail)
}
And here is the correct usage of a Guard Clause which allows us to avoid deeply nested logic.
Correct
fn read_config4() -> Result<Config, Error> {
let file = File::open("program.cfg");
// Correct use of Guard Clause
if let Err(_) = file { return Err(Error::ConfigLoadFail); }
let f = file.unwrap();
let mut buf_reader = BufReader::new(f);
let mut contents = String::new();
// Correct use of Guard Clause
if let Err(_) = buf_reader.read_to_string(&mut contents) {
return Err(Error::ConfigLoadFail);
}
let mut data: Vec<u8> = vec![];
for item in contents.
split("\n").
map(|s| s.to_string()).
filter(|s| !s.is_empty()).
collect::<Vec<String>>() {
let num = item.parse::<u8>();
match num {
Ok(conf) => data.push(conf),
Err(_) => { return Err(Error::ConfigParseFail); }
}
}
Ok( Config { data: data } )
}
If you'd like to test the examples above I have a working gist here: danielpclark/guard_clause_rfc.rs … you just need to create the config file with the contents of "1\n2\n3\n"
.
Having your code implemented with guard clauses adds a huge benefit for code clarity and avoids having your conditionals carry your code too far and deep to the right.
Improvements for Guard Clause is currently in Pre-RFC as I would like to avoid using unwrap()
after a Guard Clause.
The guard clause doesn't have to be strictly for an Error
alternate return type. I believe it can apply for any time when there are two conditional paths and one of those paths is nothing but a return type.