3 ms·
Compare: struct BinaryTree { leaf_value: int, left_child: BinaryTree, right_child: BinaryTree } function sum_leaves(tree:
by justinpombrio 3y ago
Compare:
struct BinaryTree {
leaf_value: int,
left_child: BinaryTree,
right_child: BinaryTree
}
function sum_leaves(tree: BinaryTree) -> int {
if tree.left_child != nil {
return sum_leaves(tree.left_child) + sum_leaves(tree.right_child);
} else {
return tree.leaf_value;
}
}
vs:
enum BinaryTree {
Leaf(int),
Branch(BinaryTree, BinaryTree)
}
function sum_leaves(tree: BinaryTree) -> int {
match tree {
Leaf(leaf) => leaf,
Branch(left, right) => sum_leaves(left) + sum_leaves(right)
}
}
(If you're thinking "the first example should be using inheritance + polymorphism", imagine that "BinaryTree" is in a different library than "sum_leaves". If you're now thinking "visitor pattern", sure, go write your hundreds of lines of boilerplate code if you like.)
The first example is less safe because it's filled with invariants: left_child is nil iff right_child is nil, and leaf_value should only be accessed when they're nil. The second example has zero invariants. (You might think there would be an invariant that the children aren't nil, but languages with pattern matching tend to use Optional instead of nil, so that invariant isn't necessary.) If you make mistakes about when you access various fields in the first example, you'll be accessing leaf_value when it's uninitialized, or get a null dereference from one of the pointers.
As for readability, that's in the eye of the beholder, but I find the second example a lot more readable for the same reason: it's clear in both the data definition and the use site which fields exist.
All sorts of details vary across languages, even with a small example like this, but that's the basic differences.
- marcus_holmes 3y agoThanks for the clarification. I get what you're saying, but I wouldn't write it like this - I'd write more code with more checks ;) In terms of errors, though, it's generally "the result is either a value and no error, or no value and one of these errors". I get how sum types would help with this, and I'm not arguing against that; they would be useful. But the pattern matching basically still has to deal with that outcome, and have a pattern for each error type. It doesn't strike me as being inherently safer, more readable, etc.
- justinpombrio 3y ago> But the pattern matching basically still has to deal with that outcome, and have a pattern for each error type. Not so! In Rust: enum FileError { FileNotFound, FileExplodedWhileOpening, } impl Display for FileError { ... } // say how to print the errors // Result is defined in the standard library. // It's used to store results that may be successful, or may be an error: enum Result<Success, Error> { Ok(Success), Err(Error), } fn read_file(path: Path) -> Result<File, FileError> { ... } fn main() { match read_file("kittens.png") { Ok(file) => // show kittens on screen Err(err) => println!("Could not show kittens :-( \n{}", err), } }
- meheleventyone 3y agoIn your first example you don’t need the else clause and wouldn’t you clear up a lot of the invariants by checking leaf value (with ‘not a leaf’ appropriately represented) rather than whether there is a left child. Or even representing them with a function call isLeaf. I agree that sum types are lovely and that pattern matching makes them nice to work with but I don’t think you really make the case well here that it’d be superior rather than just personal preference.
- justinpombrio 3y ago> In your first example you don’t need the else clause and wouldn’t you clear up a lot of the invariants by checking leaf value (with ‘not a leaf’ appropriately represented) rather than whether there is a left child. I'm not sure what you mean by "appropriately represented". What's an appropriate representation of "not present" if a leaf is allowed to be any int? > Or even representing them with a function call isLeaf. Let's see how it looks with an isLeaf() function: struct BinaryTree { leaf_value: int, left_child: BinaryTree, right_child: BinaryTree } function sum_leaves(tree: BinaryTree) -> int { if tree.is_leaf() { return tree.leaf_value; else { return sum_leaves(tree.left_child) + sum_leaves(tree.right_child); } } It still has an "else" clause, and it's still full of invariants. Not sure how this is supposed to be much better? > I don’t think you really make the case well here that it’d be superior rather than just personal preference. Increased type safety is not personal preference! Here's the list of errors that are easy to make in the first example, and literally impossible in the second: - accessing `leaf_value` when it's not set - accessing `left_child` when it's nil - accessing `right_child` when it's nil - setting exactly one of `left_child`, `right_child` to nil - setting `leaf_value` when `left_child` or `right_child` is nil (You might imagine that the "setting" mistakes are possible in the second example too. If Go merely gained pattern matching, this would be the case. Most languages that were born with pattern matching, though, don't have default/uninitialized values for everything, and so don't let you make those mistakes. I.e., you cannot construct a BinaryTree in Rust without choosing a value for the leaf or for the two branches when you do.)
- meheleventyone 3y ago