3 ms·
I don't understand why pattern matching is seen as more readable, safer or simpler. Why do you think this?
by marcus_holmes 3y ago
I don't understand why pattern matching is seen as more readable, safer or simpler. Why do you think this?
- macintux 3y agoPattern matching in Erlang is a brilliant feature. You get concise branching and binding, and if a pattern match fails the native error handling (dramatically less verbose than Go) takes care of things for you (mostly). Much like other FP features, shoehorning pattern matching into a language doesn't give you nearly the same advantages as building a language around it, so I don't know that it would make Go significantly better.
- marcus_holmes 3y agoI find it really, really hard to work out which pattern is matching when debugging Erlang. It might be more concise but it's massively less readable (and less amenable to reasoning out what might be going wrong). Especially in older code bases that have had a few people work on them.
- justinpombrio 3y agoCompare: 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.)