> Sure, the former are language features while the latter are library features but that doesn't seem to be a meaningful difference when it comes to comprehension.
Absolutely. The difference is that Go has a limited number of such features and once you have learnt them that's all you need to know, in that sense, and can understand any code base.
One thing which I have not expressed very well in my reply is what exactly I meant by "understanding what the code does". When you look at func fetchData(T1) (T2, error), it's easy to understand what it does: it fetches some data from T1 and returns it as T2, with the possibility of it failing, and returning an error. If you know what T1 and T2 are (which you should if you're inspecting that code), that's usually sufficient. You understand (almost) all of it's observable behavior, which is different from it's implementation details. Similarly, `abs` returns the absolute value of a number
`append` also has easy to understand observable behavior: it appends the elements starting at position len(slice) and reallocating it if necessary (generally, if the capacity is not big enough), but it's actual implementation is undoubtedly very complex. `range` is harder to explain, but rather intuitive when you get the hang of it.
Of course you also want to keep in mind the behavior of all the language primitives as well: operators, control flow etc. In Go, you have to keep all of these things in your head to understand what is happening in the code, but once you do you really understand it.
We can call all of these things: variables, language primitives, API functions etc. atoms of behavior. In Go, to understand a piece of code, you first have to understand what the observable behavior (but usually not the implementation) of all of the atoms in that code are, and then understand all of the interactions between those atoms that happen as a result of programmer instructions.
What I mean by line-level vs package-level abstraction is quite simple (maybe not the best names, but hey, I'll stick with them). With package-level abstraction, the atoms, as well as the interactions between them, remain conceptually easy to understand, but become more powerful as you move up the import tree. The observable behavior of an HTTPS GET is easy to understand, but very complex under the hood.
With line-level abstractions the atoms, and especially the interactions between them, become very complex. The programmer no longer "has to understand" the observable behavior of every single function he uses. Odd one-off mutators are preferred to inlining the mutation because it "makes the code more expressive" - in that it makes it look more like english, it makes it easier to understand what the programmer is trying to do. It does not, however, make it easier to understand what the programmer is actually doing, because the number of atoms - and their complexity - increases substantially. If you want to get a feel for this look at the explanation for any complex feature in C++ on cppreference.com. I must have read the page on rvalue references 20 times by now and I still don't grok it.
Of course, with line-level abstraction, the programmer doesn't need to constantly keep in mind 100% of the behavior of the atoms he's using, much less whoever's reading.
I can't tell you which one's better - probably both have their place - all I'm saying is that I, personally, can't work with C++/Rust/other languages in that style. I've tried to use them but I can't. C is easier to use - for me.
> So while Go may be quick to write. I think the understandably is deceiving. Yes, I can understand every line, but understanding the program/patch as a whole becomes much more difficult because of the lack of abstraction. Humans can only hold so much in our head, making abstraction a critical tool for understandable code. So while too much of the medicine can be worse that the disease I think Go aimed - and hit - far, far below the ideal abstraction level.
I think this is the fundamental point of contention. Go aims at abstraction at the package level. Each package exports a set of types and functions which are "magic" to outsiders and can be used by outsiders - an API, if you will. Rust seems to aim at abstraction at the line level - each line is an abstract "magic" representation of what it is meant to do.
In your Go code, the only pieces of line-level magic are `range` and arguably `append` (even though I would argue it is integral to the concept of slices). And of course the API magic of `fetchData` and `abs` which is in both versions.
On the other hand, Rust has .iter(), .map(), .filter_ok() and .collect(). So, while I think anyone could understand the Go code if you explained `range` to them, I do not understand the Rust code. Yes, I understand what it does, but I have no clue how it does it. What is the type of .iter()? Why can I map over it? Why can I filter a map?
But that's not the point of the Rust code. The Rust code expresses what should be done, not how it is to be done.
The way Rust deals with complexity is by offering tools which push that complexity into the type system, so you do not have to keep everything in your head. The way Go deals with complexity is by eliminating it and, when that is not possible, by making sure it does not cross API boundaries. In Go you do keep everything in your head.
This is pretty much correct, except for 2 minor gripes: Firstly, Go was meant to replace C++ in server-land. The fact that it ended up being a suitable alternative for Java/Python in many cases was a happy accident.
Secondly, I don't think the fact that Go making the opposite trade-offs in terms of (let's say) programmer effort vs. CPU clock cycles means it is a "language for the average". Go is great for any number of interesting high-level server-side components where it being done in half the time is better than it being 15% faster.
I'm going to be a smartass about this and say I think neither of you are wrong. The "extra typing" gives you a large boost in your confidence that you've written something correct, without necessarily boosting the correctness of what you have written.
The best example of this which I have seen is Amos' https://fasterthanli.me/blog/2020/i-want-off-mr-golangs-wild... in which he chastises Go for allowing him to print filenames which contain non-UTF-8 data to the terminal which does not display correctly, instead of forcing them be escaped. Rust does that, so he is confident that the program he writes in Rust handles filenames properly: if they are UTF-8 they are printed normally, if they are not they are escaped. Since the Rust type system allows him to do this, it must be correct, right? Of course not. Filenames which contain vt100 escape codes would do a lot more damage and they are not handled at all.
At the end of the day you still have to think of all the possible cases. Types help to constrain the number of possible cases, but the more time you're spending making type constraints as narrow as possible, the less time you're spending handling the cases which can never be caught by the type system.
First of all, interfaces are not nil if they point to a nil value for the same reason a * * T is not nil if the * T is nil. That is the correct decision. You can not call a function on a nil interface, but you can call a function on an interface which is not nil, but where the value of the type is nil[1].
As for your proposal, there are some issues with it: not all structures have a sane default for all not optional, nilable parameters. What is the default underlying reader for a bufio.Reader? A reader which returns zero bytes? Certainly that would be more confusing to debug than a simple panic, which is what we have now [2]. There's also the fact that a zero value is just a value with all the bytes zeroed and allocating an object via language means (new) never allocates more than the size of the object and doesn't do computation.
But I guess the main point would be that I simply do not have a problem programming with nil-able types. Failing to initialize in Go means writing var a *T as opposed to a := &T{} or a := NewT(), which seems like an odd mistake to make - or forgetting to initialize a member of a struct in the NewT() function. Fundamentally, I do not want to spend hours of my life dealing with safeguards which are protecting me from a few minutes of debugging.
But hey, that's just me. Go isn't Rust and Rust isn't Go and that's a good thing.
Never said it was a good idea. It's inefficient, unsafe and hard to read but, as far as I can tell any generic function with concrete return types can be written using reflect (and any generic function with generic return types can be written also, it just requires the caller to manually cast from an interface{} to the correct type).
As for programming with types, that's partly what Go was trying to avoid: https://news.ycombinator.com/item?id=6821389 . And I agree with Pike on this one. The nice thing about Go for me is that I'm just writing code. Not defining a type hierarchy, not rewriting some things to get them just right, not defining getters, setters, move and copy constructors for every type :). Just telling the computer what to do and in what order. When I'm writing a library I'm defining an API, but that's about it; and you can usually steal whatever the standard library's patterns are there.
I disagree about nil as well; I think Go's zero value approach is useful, and basically impossible without nil (it wasn't necessary in my code, but I may want to instantiate an ApiResponse object without a data structure ready to be passed in).
A little bit of a rambly response from me, but, all in all, I think I'll be one of the stubborn ones which refuses to use generics in his code for a long time.
> If, for example, the punishment for certain crimes is so high by default that the criminal would rather die than get a sentence that would equal death, then you have taken any and all motivation from that criminal to look for a more reasonable way out, instead preferring to "go out in a blaze of glory" on their own terms.
Yep. That is what happens in most cases. Which is why I reject the idea that most cops which shoot black people are racist. Sometimes, as is the case of George Floyd, the individual officer is at fault (even if that does not _necessarily_ mean he is racist), however, in the vast majority of police shootings, the victim is trying to reach for the cop's gun, assaulting the officer, or in some other way is putting the officer's life in danger. So you can not blame the individual shooter in such cases.
You can, however, blame the systems which cause the erratic behavior, that's true. However, we need to have an honest conversation about what the causes actually are.
The gang culture in prisons is indeed a problem, but making prisons less tough would not solve this problem - it would only exacerbated, as gangs would have increased influence over the prison.
Of course, in reality many gangs are in cahoots with the prison staff, which is a corruption problem which needs to be solved.
Another problem the article correctly points is non-violent offenders becoming violent as a result of their time in the prisons.
That said, the gang culture in prisons is only an extension of the gang culture outside of prisons. And you can not blame that on tough sentencing, as the gang culture exists in many other places around the world - in the UK grooming gangs, in Sweden's no-go zones, in Romania's gypsy gangs and so on, all countries without harsh sentencing.
You claim that law enforcement culture differs in the countries which you are comparing and that is indeed true. But is the crime culture the same? Or is that not a factor worth considering?
I've watched 30 clips. ALL of them were 30 second clips with no context. All of them from one Twitter thread as well, so not sure why that was not linked instead.
On the other hand, all of the instances of so-called police brutality with context which I've seen elsewhere occur after the police calmly order the protestors to leave a particular area, or to go home because of a curphew order or something else and the protests refuse in a less-than-calm manner.
My question is this: is the police using force to manage a crowd always unjustified in your view?
Absolutely. The difference is that Go has a limited number of such features and once you have learnt them that's all you need to know, in that sense, and can understand any code base.
One thing which I have not expressed very well in my reply is what exactly I meant by "understanding what the code does". When you look at func fetchData(T1) (T2, error), it's easy to understand what it does: it fetches some data from T1 and returns it as T2, with the possibility of it failing, and returning an error. If you know what T1 and T2 are (which you should if you're inspecting that code), that's usually sufficient. You understand (almost) all of it's observable behavior, which is different from it's implementation details. Similarly, `abs` returns the absolute value of a number
`append` also has easy to understand observable behavior: it appends the elements starting at position len(slice) and reallocating it if necessary (generally, if the capacity is not big enough), but it's actual implementation is undoubtedly very complex. `range` is harder to explain, but rather intuitive when you get the hang of it.
Of course you also want to keep in mind the behavior of all the language primitives as well: operators, control flow etc. In Go, you have to keep all of these things in your head to understand what is happening in the code, but once you do you really understand it.
We can call all of these things: variables, language primitives, API functions etc. atoms of behavior. In Go, to understand a piece of code, you first have to understand what the observable behavior (but usually not the implementation) of all of the atoms in that code are, and then understand all of the interactions between those atoms that happen as a result of programmer instructions.
What I mean by line-level vs package-level abstraction is quite simple (maybe not the best names, but hey, I'll stick with them). With package-level abstraction, the atoms, as well as the interactions between them, remain conceptually easy to understand, but become more powerful as you move up the import tree. The observable behavior of an HTTPS GET is easy to understand, but very complex under the hood.
With line-level abstractions the atoms, and especially the interactions between them, become very complex. The programmer no longer "has to understand" the observable behavior of every single function he uses. Odd one-off mutators are preferred to inlining the mutation because it "makes the code more expressive" - in that it makes it look more like english, it makes it easier to understand what the programmer is trying to do. It does not, however, make it easier to understand what the programmer is actually doing, because the number of atoms - and their complexity - increases substantially. If you want to get a feel for this look at the explanation for any complex feature in C++ on cppreference.com. I must have read the page on rvalue references 20 times by now and I still don't grok it.
Of course, with line-level abstraction, the programmer doesn't need to constantly keep in mind 100% of the behavior of the atoms he's using, much less whoever's reading.
I can't tell you which one's better - probably both have their place - all I'm saying is that I, personally, can't work with C++/Rust/other languages in that style. I've tried to use them but I can't. C is easier to use - for me.