Literally, you get a reader when you want to read something. You get a writer when you want to write something.
Though, beware thinking you can solve this with word choice.
Literally, you get a reader when you want to read something. You get a writer when you want to write something.
Though, beware thinking you can solve this with word choice.
This strains ordinary language imo:
Say you have a "log" which implements the Reader interface. When I call "log.read()" surely we are reading from the log, no? The log itself isn't the reader. This matches everyday usage.
Perhaps your argument is that you can envisage the "log" object as an abstract "reader" of the bytes on disk, or something like that -- so the mental model is not a "log being read from" but instead "bytes on disk, an object able to read them (the Reader), and the code using that object to do the reading". In this worldview one never does anything oneself, but always uses a tool to do that thing -- the "-er" tool. I dislike this as it introduces an unnecessary layer of abstraction, but I grant it resolves the issue consistently.
Also in this conception now I have a variable named "log" which does not represent the log but "a reader of the log". So you have to either ignore that inconsistency or name the variable "logReader", which introduces extra verboseness everywhere.
The other resolution is simply to say this is a programming language, not ordinary language, and we decided that having a consistent, mnemonic way to name one method interfaces was more important than respecting natural language analogies. So f it, we're using "-er" to mean "-able".
I would be curious to know what the actual conversation was during the development of Go.
So, still seems to work for me.
someFile.read(dest)
network.read(dest)
I'm reading from a file or network in both cases. You can say you're using "the reader" to read, but as I said it a) introduces an unnecessary concept, b) is not how we naturally talk, and c) does not fit with the variable names (unless you want append "Reader" to all of them, which I think is ugly and bloated.)It's not the worst thing in the world, and I understand the benefits of the choice, but still it slightly grates on me.
Consider now you send a physical letter to me? You use a mail carrier. You have my address, but sending is through the carrier. Same for calls, you use a service provider to make the call.
We perform some actions with tools, but not all. Do you use a tool to run? To read a book? You simply run, or read. My issue is that Go forces every situation into the tool paradigm.
If I have an object representing a file that I can read from, I am reading from that file. I am not using anything to read from it. So either the object can no longer represent the file directly (but instead a tool used to read it), or the file can keep representing the file but the language is off because the file is now also a "Reader" (in the Go sense) of the file.
> You are using some other code capable of reading from the file, though.
We are talking about how to conceptually name pieces of our code to match our natural concepts and language. That is, the conversation we're having is one about modeling, so this move seems illegal to me. The "other code implementing reader" is not part of the model. It would be like arguing, in regular life, that "you can't say that you read a book, because really it is your brain doing the reading".
It can fail if you are writing the physical contact point to the hardware, but very few of us do that. We use code that does that.
Do I use a tool to read? I have reading glasses. And an ebook reader. And I've delegated some reading to others. Life is complicated. Language no less so.
Your ebook reader it the thing being read, not the thing that reads. Glasses for reading are also called readers, and they are the tool helping you read. Here, to your point, we see "language being complicated" -- the same convention being used for opposite things. But this is a tangential point leading us astray.
I thought about this some more in the shower, and I think I can distill my objection more clearly.
Methods on objects act on those objects. That is:
object.do()
is equivalent, in English, to performing the action "do" on "object". To take a random example from Effective Go: d, err := f.Stat()
if err != nil {
f.Close()
return err
}
We "Stat()" the file (get statistics about it) and we "Close()" the file. This pattern is well-established in both Go and nearly every language with objects. Instance methods are actions performed on their object.Now of course "File" also implements "Reader". Yet when I "f.Read()" the situation is exactly the same as in "Stat" and "Close". I am reading the file.
So where is the "Reader" now? Well the Reader is also the file. In the conceptual model, there is no other object around. The file is both the Reader and the object of reading. And that's my problem. The "reader as tool" is inconsistent with "methods act on objects".
In the tool model you've been arguing for, I would have to write something like:
reader.read(file)
The redundancy of that aside, this is not typically what you see with Go code (though you sometimes do). You are more likely to see stuff like "f.Read()"As I said, you can argue that Go's "File" object is not meant to represent a file, but instead an abstract tool: a thing which can Read and Stat and Close files.
But that's not what the documentation says: "File represents an open file descriptor."
That is, in the intended conceptual model, the "f" in "f.Read()" is a file. And we are reading it. And that is at odds with it being a "Reader" -- a thing that reads.
bytesRead = read(fd, buf, BUF_SIZE - 1)
You read the from the file descriptor into the buffer. Just like in Go you read from a file into a byte array. Neither the file descriptor nor the file is conceptualized as a tool for reading. The file descriptor is merely an abstraction of the file, which extends the concept of "file" to include pipes, sockets, and other io.In this sense, the GP is right - to respect OS terminology, in Go we should write
fileReaderWriter := os.Open(path, "rw")
Since os.Open doesn't return a file handle/descriptor, but an object capable of reading or writing to a file. This is different from C, where open() does return a FILE*, which is just a handle to a file which you explicitly pass to read() or write() or mmap() etc.The model is similar with Go's interfaces. You can use a FileReader to read from a file on disk and give you the data. You didn't read the data yourself, the Reader read it for you and put it in a []byte.
Interfaces are always named for what purpose they serve to others, not for what they do internally.
I do agree that the File name is misleading for what os.Open actually returns. Java's FileStream name seems to capture the concept more clearly to me. However, I think this is am intentional choice on the part of Go designers, who are somewhat allergic to object-oriented design. This is the same reason they don't want the receiver of a method to be named "this", even though that is exactly what the code models.
Basically, the Go designers want to pretend that Go struts are not objects, they are only data, and that methods on that data are kinda just free-floating functions with some extra syntax sugar. Of course, that is not what Go actually does - a Go struct instance with methods and private fields is exactly equivalent to a Java Object instance and completely different from a C struct instance. But they don't like that framing.
You'll see this all over the Go stdlib - struct are named after the data they represent, but then they also implement interfaces named for the purpose they serve to others.
So what is an instance of the struct named File? Go has chosen a dualistic perspective, like with the particle/wave dualism in physics. In some experiments, a File is just a representation of an open file, that you pass to other methods to do something with (e.g. ioutil.ReadFile(file)). In other experiments, it is an object which can read data from the OS and return it to you (e.g. file.Read(), or ioutil.ReadAll()).
> Go has chosen a dualistic perspective, like with the particle/wave dualism in physics.
This summarizes my complaint. It's not consistent. It forces me to switch between perspectives and keep two contradictory models in my head. As I said, it's not the worst thing -- I can deal with it. But I consider it a kind of conceptual wart in the design, or at least of the naming conventions and idioms.
A final pedantic point. I find your interpretation of "ebook reader" interesting, and grant that it is a valid one, and fits with the Go "thing as tool" perspective. However, I don't believe it is the perspective most people use in everyday life, and I think the dictionary definition makes this clear:
e-reader (noun) -- a portable electronic device used for reading books and other text materials that are in digital form.
That is, and e-reader is the thing, the object, the book-substitute that you read.
From: https://www.dictionary.com/browse/ebook%20reader
EDIT: on 2nd thought, maybe that definition does back up your interpretation. "device used for reading books..." I still feel like in practice it is modeled in people's minds just like a "different kind of book" but maybe that is just me, or just some people....
EDIT 2:
> In other experiments, it is an object which can read data from the OS and return it to you (e.g. file.Read()...)
My other objection here (as I pointed out elsewhere) is that if I am supposed to take the "interface as tool" perspective when interpreting the "Reader.Read()" method, then the variable "file" is misnamed. I read a file, I don't use a file as a reader to read a file.
Since I can only name a variable according to one perspective, the name will always feel wrong when I switch to the other.