1. 32
  1. 9

    This is a good exploration of deficiencies in the Go I/O APIs (it boils down to “it’s just like C”, essentially), but this post suffers from way too many code examples.

    1. 8

      90% of this post is a warm-up for the bug near the end. It starts with small flaws that seem insignificant, but they all add up to a messy bug in a pile of shoddy code.

      1. 1

        I think you mean this also as a reply to @GeoffWozniak

      2. 5

        Let’s build a strawman just like the one the author built, while remembering to adhere to the theme of ignoring all nuance and real-world usage of said API.

        <strawman>

        We endeavor to find out what happens if we call next() again on a Rust Iterator that has just returned None. Let’s read the documentation:

        Returns None when iteration is finished. Individual iterator implementations may choose to resume iteration, and so calling next() again may or may not eventually start returning Some(Item) again at some point.

        This seems a little difficult to program against, no? ”…at some point”? And when might that be? Should I go read the particular documentation for the iterator I’m using? And I’m writing generic code, then, certainly, the only thing I can do after I’ve seen None is stop immediately. But if that is the case, and I have the strong Rust type system on my side, why does reaching the end of iteration still leave me with an Iterator value in an unspecified state? Shouldn’t it somehow arrange to be destroyed / consumed if it cannot be used further? In the words of the author:

        Oh no. That’s one situation where “there’s options” is definitely a bad thing.

        What ever shall we do with this weakly specified Iterator interface? It seems so terrible to program against!

        </strawman>

        Thankfully, in reality, very little Rust code needs to worry about this, because most Rust code never calls next() on iterators directly, since the convenient forin construction exists. Much like most Go code never calls Read on an io.Reader directly, since the convenient io.Copy, ioutil.ReadAll, io.ReadFull, etc. constructions exist. Furthermore, I’d venture to say that for ... in on an iterator in Rust and consuming an io.Reader in Go are comparable operations in terms of their ubiquity.

        But that’s no excuse for having such a poorly specified interface!

        I very much agree, but such is the abstraction level of the language that it cannot reasonably specify it any more precisely than that. The Go language has many flaws (have you seen context.Context???). But it seems to me that the design of io.Reader is, in general practice, such an irrelevant case of “this interface is not specified as strongly as it could be”, that I cannot understand the author’s desire to devote thousands of words to it.

        Making APIs resistant to misuse is certainly a good objective, but if the author has a bone to pick with Go for promoting such APIs due to its weak type system, there are many other easier and more practical targets. As “poorly” specified as it is, I doubt many Go users ever misuse io.Reader by calling its Read method directly.

        1. 7

          I don’t think this is a strawman and I think it is a valid criticism of Rust’s Iterator interface. Rust in fact recognizes this and there is a FusedIterator trait which guarantees None to be final.

          You are right Rust’s for loop saves most users from this complexity, but whether to fuse or not IS a significant design concern whenever you are on the side of implementing an iterator. In my experience, Rust peope who have implemented an iterator is aware of this.

        2. 4

          Yet another pointless post making the same tired arguments against Go. “It isn’t Rust and therefore it is bad” repeated 50 times in a row is not novel. People wrote these posts years ago and the response from the Go community was clear: there’re tradeoffs and they aren’t interested in making them in this space. Not every language should be or wants to be like Rust. Go isn’t trying to cargo cult functional programming like Rust is.

          The author seems to completely ignore that it’s not bad design that you can do some work AND indicate an error. Why would that be bad design? That’s excellent design because it’s what actually happened! Rust makes it hard to represent partial success

          1. 9

            I think the article is more about highlighting the tradeoff. We all agree there is a tradeoff, so what’s wrong with highlighting it? There is also perception unlike those who design and implement Go, many who use Go is pretty unaware of the tradeoff. I was certainly unaware of this particular debug/pe bug.

            1. [Comment from banned user removed]

              1. 6

                I agree it can be boring and repetitive, but I disagree it adds nothing. This is very similar to recurring “how do I turn off borrow checker” discussion in Rust. There is zero chance it would lead Rust to turn off borrow checker, but most people are interested because it can highlight where can Rust do better, slightly advancing the tradeoff front line.

            2. 3

              Rust doesn’t make it hard to represent partial success. You would just return a tuple similar to how you would in Go: fn do_stuff() -> (Option<Data>, Error). Working with that signature is certainly less ergonomic than working with a Result, but it’s not particularly hard. You don’t even strictly need the Option in a lot of cases, but I would include it to differentiate partial success and no success in the type system.

              1. 1

                You can also just return the same type as in Go, yes, but that somewhat defeats the purpose does it not?

                1. 4

                  I don’t completely follow. The benefit I see is that I can tell via the type system that a function can only return data or an error by it using a Result and that a function can return data and an error via a tuple. It’s very hard for me to misuse the former, and the latter makes it clear that other states are possible. The fact that I would have just used a result if the function could only return data or an error communicates that the API has more possible outcomes to consider. In a public API, I would probably define an enum to explicitly declare all valid states:

                  enum Fallible {
                    Ok(Data),
                    Part(Data, Error),
                    Err(Error)
                  }
                  

                  which is certainly more verbose, but I wouldn’t describe as ‘hard’.

                  1. 1

                    Yes, everyone agress there is benefit. Everyone also agrees there is cost, whether it is hard or verbose. Verbosity is also cost. Different people do value different things.

                    1. 3

                      I didn’t claim there was no cost. I was just noting the narrow point that partial success in Rust is not particularly hard. I definitely agree the linked article did a poor job meaningfully engaging with the downsides of a more expressive type system.

                  2. 3

                    You have to explicitly encode this state, and it must be explicitly handled. It’s not like Go where you can be loosey-goosey in getting both then just doing as you want with it.

                    1. 0

                      Yes and explicitly encoding it is just not worth the time or effort. It only very slightly reduces the chance of bugs for a lot more effort.

                      But again none of this discussion is new. It’s the same old religious language war between Go and Rust etc. All these points have been discussed to death.

                      I like being loosey goosey. You don’t. You can’t expect everyone to be like you.

                2. 1

                  Rust makes it hard to represent partial success

                  We embraced GraphQL at work, and now we’re faced with API responses on the form:

                  {
                     data: { … }
                     error: { … }
                  }
                  

                  It’s possible to get both data and error back at the same time – partial success. But who would use this?

                  Maybe some people use lots of query/mutation batching so you send a bunch of things together? Or maybe for very specific data/error combos I could handle a partial success and make UI code that handles that specific situation.

                  For the general case though, what do I do if I get data and error?

                  I can’t shake the feeling that partial success makes more sense for server side API, or library author rather than consumer of that API/lib.

                  1. 1

                    I’m not really thinking about web API stuff but about things like reading from files on USB sticks that are pulled halfway through for example.

                3. 2

                  One should not only focus on what errors a language allows in theory but what errors people using the language make in practice.

                  Obviously, if you can prevent errors at compile time without making the language more complicated, that’s the best solution but usually there are costs. And that you have to think about the trade off.

                  I didn’t even program golang much and the error ignoring code still stands out. Just because other languages have exceptions, it doesn’t mean that a minimally trained golang developer would ignore this. I didn’t continue reading after that point.

                  1. 1

                    Yea, someone not checking or ignoring an error (res, _) will stick out like a sore thumb after a few weeks with Go.

                    No such luck with Exceptions getting thrown around, maybe someone catches them, maybe they won’t.