Yes, agreed: using pattern matching means that your code does the right thing because of its structure. The Option
enum is a good example of this. The enum has two variants None
and Some(T)
. Because of this, you cannot accidentally use an Option::None
value as if it was an Option::Some
. That is, the syntax forces you to unpack things:
let elem = match some_list.last() {
None => ... // handle this case, perhaps with an early return
Some(e) => e // handle this as well, perhaps just pass `e` through
}
Contract this with Abseil's StatusOr
class for C++. There you get a absl::StatusOr<T>
back which is a pointer-like type. You must then check if it is ok()
and only then can you dereference the value:
StatusOr<Foo> result = Calculation();
if (result.ok()) { // We could leave this check out by accident!
result->DoSomethingCool();
} else {
LOG(ERROR) << result.status();
}
Here, it is the programmer who must remember to check if the StatusOr<T>
value is of the right variant before accessing the wrapped T
value. In Rust, it's syntactically impossible to get access to the T
value in an Option<T>
without checking if the Option
is of the right variant.
Now, if the pattern matching is spread around the code, it might very well be annoying to use:
let hit_points = match unit {
Unit::Soldier => 20,
Unit::Catapult => 35,
Unit::Tank => 1500,
};
let vulnerable_to_fire = match unit {
Unit::Soldier | Unit::Catapult => true,
Unit::Submarine => false,
};
You can then of course move this logic to methods on the Unit
enum itself:
impl Unit {
fn hit_points(&self) -> i32 {
match self {
Unit::Soldier => 20,
Unit::Catapult => 350,
Unit::Tank => 2500,
}
}
fn vulnerable_to_fire(&self) -> bool {
match self {
Unit::Soldier | Unit::Catapult => true,
Unit::Submarine => false,
}
}
}
This will at least move some of the sprawling complexity back near the definition of the Unit
enum.
Alternatively, you could define a trait for your units:
trait Unit {
fn hit_points(&self) -> i32;
fn vulnerable_to_fire(&self) -> bool;
}
You can then implement the trait for different structs/enums and run your game logic in terms of the behavior exposed by the trait. I think that might fit the SOLID principles you reference a little better.