Hacker News new | past | comments | ask | show | jobs | submit login

The problem is that it's so incredibly inflexible and dated.

Many effectful actions e.g. reading from a file system can have a range of different errors each of which you want to handle differently e.g. out of disk space versus lack of permissions.

It's great that you're treating errors as values. But you need pattern matching and other techniques as they make your code more: (a) readable, (b) safer, (c) simpler and (d) less verbose.

The hilarious thing is that eventually Go is going to get these because there is nothing but upside. And then at that point you're going to wonder how you ever survived without it.




> But you need pattern matching and other techniques...

The go way to do this would be:

    switch {
        case errors.Is(err, outOfDiskSpaceError):
        // handle out of disk space error
        case errors.Is(err, lackOfPermissionsError):
        // handle lack of permissions
        ...
        default:
        // do the equivalent of the `_` case in a scala match statement or `t` in a lisp cond
    }
Which is roughly as readable as scala/rust's match statements. Moreso if someone tried to get cute in scala and bind both the object as a whole and parts of it at the same time ( something like `case x @ Type(_, _, y, _)` ) or when people get real cute with the unapply method.

I mean, I like scala. It's fun. But I would never say it's more readable than go. I've been left to support things that happen when a team of average intelligence devs get a hold of it.

You're also really comparing an MIT and New Jersey style solution here, and judging them both on MIT merits. And I don't think that's exactly a fair argument.


Trying to compare a switch statement to proper pattern matching is like comparing a rock to a rocket.


If you're thinking of C/C++ switch statements and everyone that blindly copied them (looking at you Java, JS and PHP), you're right, but Go's switch is much more flexible (https://gobyexample.com/switch).

PHP tried to correct the mistake of not giving some more thought to the switch statement at the beginning by including a new "match" expression in PHP 8 - fun times for everyone who used classes called "Match"...


Nowadays Java and C# switch are almost as powerful as Standard ML match.


It's still a rock. Play with a language with proper pattern matching, Rust, Haskell, Ocaml, Scala, and you will also see it's just a rock.


A switch or case statement is a restricted form of pattern matching for simple values. It fits those use cases, which are frequent.

I use a language that has both pattern matching and case constructs. I use both.

If you're doing complex pattern matching all over the place (especially matching the same sets of cases repeatedly in multiple locations in the code), maybe your design sucks. It's not making effective use of OOP or some other applicable organizational principle.


> Trying to compare a switch statement to proper pattern matching is like comparing a rock to a rocket.

Yeah, but when you need to bash someone over the head, the rocket suddenly isn't any use anymore.

IOW, sometimes the simpler thing is better.

In fact, in practice, the simpler thing is usually better... Like, rocks are usually more useful to the average person than rockets are.


Go's switch statement is closer to lisp's cond expression, except it doesn't evaluate to a value.


I am unashamedly stealing this one, it is both apt and making me smile. Kudos


I don't understand why pattern matching is seen as more readable, safer or simpler. Why do you think this?


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.


Thanks 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.


> 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),
            
        }
    }


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. 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.


> 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.)


> Increased type safety is not personal preference!

Sure it is! People literally make this choice all the time. If leaf can be any int then I’d suggest a value that determines whether the value is a branch or a leaf. Which is drum roll how tagged unions work anyway.

You still don’t need the else clause with the early return.

Just to be clear the sum type/pattern matching is nice! But it’s perfectly acceptable to live without it and the world won’t end.


Pattern 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.


I 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.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: