Hacker News new | ask | show | jobs
by Merovius 3086 days ago
> you end up having to frequently write type-switches which are checked at runtime to do any sort of generic code.

This baffles me. I think I basically never use any type-switches, with the exception of interfaces being used as a sum-type - in which case the problems you mention with type-switches just don't come up.

> Case in point: to do something as painfully simple as reading a file, you need to import bufio, io, io/util, and os.

I don't know what you mean here. `ioutil.ReadFile` reads the whole file, done. Even if you prefer linewise-scanning, you still only need `bufio` and `os`.

But even if you'd need all those packages to read a file: So what? Like, I honestly don't understand what's the problem with that.

1 comments

The problem with that is I have to care how `file` works.

Here's how to read a whole file then loop over the lines:

    file, err := ioutil.ReadFile("data.txt")
    // some error handling
    for _, line := range strings.Split(file, "\n") {
        fmt.Println(line)
    }
Here's how to stream a file one line at a time:

    file, err := os.Open("data.txt")
    // some error handling
    defer file.Close()
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
    if err := scanner.Err(); err != nil {
        // more error handling
    }

Here's how I, at least, would like it to work:

    fileContents := file.Read("data.txt")
    for _, line := range fileContents  {
        fmt.Println(line)
    }
    if fileContents.Err() {
      // some error handling
    }

    fileContents := file.ReadStreaming("data.txt")
    for _, line := range fileContents  {
        fmt.Println(line)
    }
    if fileContents.Err() {
      // some error handling
    }
The critical point is that I don't want to care whether `file` is a byte slice or a byte buffer, and Go doesn't let me not care. I want to be able to write code that deals with "enumerable data of some sort", once, and then works no matter how the caller decides to provide that data.

Go (intentionally) makes it exceedingly difficult to obscure how a piece of code works from the rest of the codebase, and I personally think that's a fatally poor design decision. In my experience, it makes it difficult to decouple modules since code often has to be at least somewhat aware of quite a few implementation details of a library in order to use it correctly. It makes it really difficult to build higher level abstractions that don't leak. I have a much harder time in Go getting away from thinking in `int`s and `floats` and staying terms of the domain objects that I actually do care about.

"How" a piece of code functions is at best the third, and probably only the fourth most important question (behind "why", "what", and probably "when" if you use any concurrency at all), but Go forces it to be front and center at all times.

> Here's how I, at least, would like it to work:

I still don't get what your problem is. It seems what you want is to just take an io.Reader and pass that to bufio.NewScanner, solving your problem and letting your caller figure out what Reader to pass you? I mean, to me, this seems to be a solved problem and exactly one of Go's major strengths.

> it makes it difficult to decouple modules since code often has to be at least somewhat aware of quite a few implementation details of a library in order to use it correctly.

You still haven't described a single piece of your code that requires, in any way to know any implementation details of any of the libraries you are using. Like, you don't have to care how os.File is implemented, it just gives you a Read method that you can use to read from it, just like a thousand other Readers. And then you can use that in a bufio.Scanner to read lines (words, whatever tokens), without that having to care in any way about how the Read method is implemented. You want to scan lines from a byte-slice, use bytes.Reader, that's it's sole purpose and your scanning code does not have to care what Reader it gets passed.

Like, I seriously don't understand your problem here. It would seem to me, what you are describing is exactly how Go works.

> "How" a piece of code functions is at best the third, and probably only the fourth most important question (behind "why", "what", and probably "when" if you use any concurrency at all), but Go forces it to be front and center at all times.

Sure, I agree that Go does not encourage you to build deep abstractions. But I fundamentally disagree that you have to know any implementation details - anymore than any other language. Yeah, the type system doesn't lend itself to build extra abstractions, but "having to care about implementation details" just is not one of the symptoms of that o.O

> You still haven't described a single piece of your code that requires, in any way to know any implementation details of any of the libraries you are using. Like, you don't have to care how os.File is implemented, it just gives you a Read method that you can use to read from it, just like a thousand other Readers. And then you can use that in a bufio.Scanner to read lines (words, whatever tokens), without that having to care in any way about how the Read method is implemented. You want to scan lines from a byte-slice, use bytes.Reader, that's it's sole purpose and your scanning code does not have to care what Reader it gets passed.

These are literally implementation details. Except you're having to implement them yourself. Reading a file delimited by tokens is a solved problem. There is zero reason why I should be having string together code from four different modules to accomplish this myself. This is the entire reason we have come up with the concept of abstraction.

> Except you're having to implement them yourself.

This is plainly wrong. All components I mentioned exist in the stdlib.

You have to glue them together yourself, sure, but that's the point of having components with separated concerns, which is usually considered a good thing in software engineering.

> There is zero reason why I should be having string together code from four different modules to accomplish this myself.

You don't. You have to use at most 2. And also, to repeat the question: who cares? Like, what is the actual downside of having to import 2 packages? I also took the liberty of looking for solutions to how to do this in other languages. Here is a Java solution, which is in line with what's requested that has 4 imports and one of them is third-party: https://stackoverflow.com/a/1096859. Here's rust code with 4 imports: https://users.rust-lang.org/t/read-a-file-line-by-line/1585. Python and Haskell get away without imports; because they just make reading files a language-builtin/part of the prelude, which TBQH is pretty cheaty.

Like, even if I'd buy into the notion that modularity and composability are bad things, it's not even as if Go would be in any way an outlier here. And even if it where then at best this is the mild complaint that no one has yet wrapped this in a ~10-line library; the language certainly does allow it, contrary to what's claimed.

I'm sorry, but this complaint is just forcibly trying to make up a problem where none exist to fit your narrative of Go being a bad language. It's not productive.

(In the Rust code, the Path import is unnecessary, and rustc will warn you that you should remove it, so it ends up having three.)
In this case, I suppose my specific complaint is that I'm unable to make `range` work transparently with an arbitrary type.

I guess you could chalk it up to a difference of aesthetic opinion. I don't want to think about buffers or readables. I want to think about loops and strings. Looping over a collection means `for range`, so I'm gonna assume that Just Works. Maybe `for range` is just syntactic sugar for `bufio.Scanner` under the hood, but I don't want to care while I'm using it.

I want to think of a file as a black box full of strings. What's actually in the box? Don't care. How do I get lines out of the box? `for _, str := range blackBox`, same way I loop over every collection. How does that actually work? Don't care. Whoever implemented the box has to care, of course, but I sure shouldn't. I've got more important things to worry about, like whatever it is I actually want the code to do. Every character that isn't about whatever it is I actually want the code to do is a problem.

Having primitives and builtins that only work sometimes (specifically, with a short list of builtin types and aliases for same) means I can't just use the builtins without thinking about what I'm using them on. Having to crush down to a lowest common denominator means that what's in my brain while I'm reading and writing code isn't strings and what I actually aim to do with them, it's how the Reader API works, and whether I read a full or a partial line, and whether I need to handle errors before or in or after the loop this time. I want to think about my problem domain, but Go keeps dragging me down into the weeds.

> In this case, I suppose my specific complaint is that I'm unable to make `range` work transparently with an arbitrary type.

Sure, fair enough. But note that you've now shifted the criticism from "I have to import 4 packages" (which was wrong) over "I need to know implementation details of packages" (which was wrong) to "I don't like that Go doesn't have operator overloading".

> I want to think of a file as a black box full of strings.

And what exactly is preventing you from doing that? Like, how exactly is the language preventing anyone from providing this much higher level API? You could even make it work with range, if you so desire (it would be considered very unidiomatic, but presumably you don't care).

The stdlib provides you with composable pieces to achieve the job you want. I still find this complaint incredibly weird, unless you assume that everyone wants to view files as just a bunch of lines (I'd argue, these days, the overwhelming majority of files probably aren't). Like, you will still need the lower-level API; where's the problem with having an stdlib which focuses on providing composable pieces and then having some library do the composition for higher-level concerns?

Last time I checked, having small composable units of code with clearly separated concerns was pretty much universally considered a good thing.

> it's how the Reader API works, and whether I read a full or a partial line

This is just a random aside, but: You never have to care about that, unless you specifically want to. But I would argue that code that calls io.Reader.Read is likely wrong - unless it does so to wrap it. Use io.ReadFull.

Go can offer simplicity and flexibility only once you use function literals. Like reading a file line by line could be just a single function call that calls your function literal each time the line is read.
Yes, thank you! This is the point I was trying to get across. In a sense, Go makes all abstractions shallow. Perhaps worse, Go makes all abstractions several times leakier than they need to be. While this is apparently something the "all code must be explicit" crowd seems to love, to me it means I can't just focus on the high-level business problem I'm trying to solve. Instead I have to get bogged down in the nitty gritty details of everything.