The Value in Go’s Simplicity
benjamincongdon.me
benjamincongdon.me
Everything in moderation of course. For me personally simplicity of go is too much and I don't feel comfortable writing it.
I would challenge this. I would say _some_ of it is wasted, but most of it works towards making the code more understandable. And code being understandable to the next person to read it (both in the small "what is this block doing" sense and in the larger "what is this algorithm doing" sense) is very important.
Even Go did this by adding async features into the core language. This is a complication that doesn't exist in the older languages it was intended to build on, but by building that complexity into the language, they reduced the burden of using it in your code.
Go's simplicity and heavy idiomatic culture means all the code more or less looks identical. This is great for team projects.
http://joeduffyblog.com/2016/02/07/the-error-model/#forgetti...
> It’s surprising to me that Go made unused imports an error, and yet missed this far more critical one. So close!
Result/Option/Maybe types force unwrapping, which makes ignored return codes auditable and allows you to manage technical debt.
This doesn't speak to Go's simplicity so much as it does to Go's conservatism. Having the Go standard library use sum types, establishing a precedent and a culture, would be no more complex in the absolute, but would have been more of a stretch for its initial target user base.
The most egregious to me has always been that unused imports are an error but variable shadowing is not.
Even in languages with dynamic side-effecting imports (like Ruby or Python) I've never seen a bug caused by an unused import. Not so for shadowing (don't get me wrong, it's a convenient feature, but if you're going to remove this sort of things because reasons it's a much bigger pitfall than unused imports).
import "foo"
func other() {
shadow var foo string = "bar";
}[0] and go is actually weirder than that — and the opposite way 'round — as `var` doesn't allow any shadowing in the same scope:
var a, b int
var a, c int // fails because it redeclares a in the same block
while `:=` allows same-scope shadowing as long as the overlap is not complete: a, b := foo()
a, b := foo() // fails because no new variable on the left side
a, c := foo() // succeeds
both allow arbitrary shadowing in sub-scopes. a := 2
a = 3
By definition you must have nested scopes to have shadowing. Within the same scope, it's only ever assignment. let a = 2;
let a = 3;
I think you would say the latter shadows the former...There is already a “let” to show you that a variable is being created, adding more verbosity to a feature that, in some sense, is about removing verbosity kinda misses the point, in my opinion.
That said, never say never, but if I was a betting kind of person, I’d bet against it ever being accepted.
For example I often write code like this in Java:
String ageString = request.getParameter("age");
int ageInt = parseInt(ageString);
because I can't re-use name `age` twice and forced to distinguish between those names.Now I agree with you about imports. I often want to comment a line and run program. Now I have to comment a line, run a program, encounter compilation error, find import, comment that import, run again. And uncomment two lines later. With Java my IDE optimizes import and removes all unused imports when I'm commiting my code. While I'm working on my code, I'm absolutely fine with any warnings. I would say even more: back then when I used Eclipse, it had awesome ability to compile even code with errors. This code just throws exception on runtime. But if I'm not really interested in that snnippet and working on other part, I can run it just fine. Probably that feature is the only thing that I'm missing from Idea.
Shadowing exists in most languages, the biggest difference being the allowed scope relationships between the shadower and the shadowee: most languages allow inter-function and inter-block shadowing (if they have actual block-level scope so e.g. not Python).
Intra-block shadowing is a much rarer feature, and one which Go doesn't have.
I use it in most of my open source projects.
You could give this dialect a name and then maybe the compiler could enforce rules on projects that declare that they’re using that dialect (like C compilers do with dialects like “c19” vs “gnu99”), but it’s not strictly necessary; you can also just create “dialect tooling” that wraps the language’s tooling and adheres to those rules (like Elixir’s compiler wraps Erlang’s compiler.)
And a CI shell-script, or git pre-commit hook, that runs a linter before/after running the compiler, is an example of just such “wrapped tooling.”
I think this really hits the nail on the head. There are benefits to a conservative approach, but it's not the same as simplicity.
I understand the kind of joy-of-expression that lives in things like https://poignant.guide/dwemthy/, but if you attempt to put that in a production codebase, I’m not gonna be at-all positive in code review.
Save art for the canvas, for your weekends, for your loved ones. Bring a professional self to your job.
The way people fail in this line of work, if they have skill, is burnout. Burnout is the thing that'll get you. So you do whatever you can to stave it off - and that requires working on something you actually care about in some way.
This is the hallmark of a professional (very likely mature and senior) team member. Nothing to prove and interested in a maintainable project.
There are times for clever, no doubt. Every rule has an exception.
Bill (William) Kennedy at Ardan Labs has a line he uses in his talks: the bottom level developers need to grow and come up and the top level developers need to avoid cleverness and come down and everyone meet in the middle.
If you think this is different than what the parent is describing then you’re doing it wrong
Some languages have a big bag of tricks, other languages let you extend them to the moon, and still others make you work at it a little. In the end it's all just computation, and the tool choice can be reduced to a list of "must haves" and "cannots". If you need more expressive power -- make your build a little more complex and start generating code, give it a small notion of types or static invariants. It only has to generalize as much as your problem does, and that leads you to build the right abstraction instead of dumping an untried language feature on the problem in the hope that it is a solution.
"I want to interact with a REST API and pull a field out of its response JSON" is an incredibly common workflow, and yet to do that in golang is far from trivial. You need to define serializer types and all sorts of stuff (or you can take a route I've seen encouraged where people to use empty interfaces, which can cause runtime exceptions).
Same deal with a worker pool. Concurrency is great, but instead of providing a robust, well written solution as part of the language itself, it gives you a toy like this https://gobyexample.com/worker-pools (still the most common result on Google) that is only 80% of the way there. Then you find yourself bolting things onto it to cover your features (we need to know if things fail, so let's just add another channel. We also need finality, whelp, another channel it is), and before you know if you have an incomprehensible mess.
// Interact with a REST API and pull a field out of its response JSON.
func interact(url string) (field string, err error) {
resp, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("error making HTTP request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("error querying API: %d %s", resp.StatusCode, resp.Status)
}
var response struct {
Field string `json:"the_field"`
}
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return "", fmt.Errorf("error parsing API response: %w", err)
}
return response.Field, nil
}
IMO this is a good level of abstraction for a language's standard library. Each of the concrete steps required for the process are simply and adequately represented. Each can fail, and that failure is idiomatically managed directly and inline, which, when applied to an entire program, significantly improves reliability. If you find yourself doing this often, you can easily write a function to do the grunt work. Probably func getJSON(url string, response interface{}) error
> Same deal with a worker pool. Concurrency is great, but instead of providing a robust, well written solution as part of the language itself, it gives you a toy like thisGo's concurrency primitives are very low level. This can be bad, but it can also be good: not all worker pools, for example, need the same set of features.
I always thought code written as page after page of low-level details was bad code. I thought the same thing about code written as class after class after class of OO hierarchy. But people talk about Java and Go as if it's impossible to write unreadable code in them. I don't think code has to contain a single hard-to-understand statement to be "unreadable." After all, code that is unreadable because of abuse of powerful language constructs isn't literally "unreadable." You call it that because reading it requires an unreasonable amount of time and effort. The same thing can (and should) be said about code that requires an unreasonable amount of effort for any reason.
To me, it's just different ways that programmers can waste your time. One programmer might waste your time by combining powerful language features in cryptic ways; another might waste your time by hiding crucial structure in a vast sea of details. What's the difference?
I've found "go doc" amazing for this use-case; I only ever trawl through the source-code for a high-level understanding as a last resort - usually because the code is undocumented or under-documented.
I completely agree with this. The best code is code you don't have to read because the structure of the code makes navigation easy and functional boundaries obvious. A language that doesn't provide strong support for declaring functional boundaries results in code that is much harder to read because you have to comprehend a lot more of it to know what's going on.
Few months ago I started working on a Go codebase. Yes, while the language is simple, you can absolutely make code confusing. If you wrote code yourself it is obviously simple to you, because you know the structure. But it can be a nightmare to someone else who needs to learn the structure and only has the code.
However, Go's poor type system also forces you to write worse and less safe code than you want.
For example, the lack of enums and sum types means it's hard to represent, safely and efficiently, anything that should be the same type but have different values within a strict set of allowed values. The nearest you get in Go to an enum is:
type Color int
const (
Red Color = iota
Blue
Green
)
Alternatively, can do the same with a string: type Color string
const Red Color = "red"
// etc.
This gives you some type safety, since a Color requires casting to force an int or string or whatever to become it. But that doesn't give you real safety, since you can do: var c Color = Color("fnord")
This comes up whenever you're handling foreign input. JSON unmarshaling, for example. You can override the unmarshalling to add some validation, but it won't solve your other cases; it will always be possible to accidentally accept something bad. Not to mention that the "zero value" of such a value is usually wrong: type Color string
var c color // Empty string, not valid!
A more safe way is to hide the value: type Color interface {
isColor()
}
type colorValue string
func (colorValue) isColor() {}
var Red Color = colorValue("red") // etc.
Now nobody can create an invalid value, and the zero value is nil, not an invalid enum value. But this is unnecessarily complicated and shouldn't be necessary in a modern language in 2019.The case of genuine sum types is worse. Your best bet is to use sealed interfaces:
type Action interface {
isAction()
}
type TurnLeft struct{}
func (TurnLeft) isAction() {}
type MoveForward struct{
Steps int
}
func (MoveForward) isAction() {}
There are some downsides. You have to use static analysis tools (go-sumtype is good) to make sure your type switches are exhaustive. You get a performance penalty from having to wrap all values in an interface. And if you're going to serialize or deserialize this (e.g. JSON), you will be writing a whole bunch of logic to read and write such "polymorphic" values.It’s not a pretty solution from a language design point of view, but it’s been more than effective for us from an engineering point of view: since we’d need those proto definitions anyway, why bother writing our own?
Like most systems guys, they know of how C handles types and how C++ handles types. The concept of Parametric Polymorphism or sum types outside of OOP is most likely something Rob Pike was not familiar with as it's hard to see why sum types were not included in the language.
Having IO functions return a tuple of error and value rather then an Option type is not simplicity it's complexity arising from lack of a type primitive. The feature is ugly and very much looks like it was implemented by someone who wasn't aware of a type that can be a Value OR an Error. So instead he implemented a type that can be an Error AND a Value and left it to manual run time checks for the programmer to figure out if an error even occurred.
The other thing is that this "tuple" type of error AND value looks like a hack. Tuple types can't be saved to a variable in GO and can only be returned by functions and immediately unrolled into its separate primitive values. It's like Rob knew something was off in this area so he created a temporary concept of a tuple in the return value of a function to make up for it. A consistently designed language wouldn't have a Tuple only returnable by a function call. It seems strange and inconsistent.
Additionally, the fact that, in GO, some types can have Nils and other types default to a zero value implies that Rob Knew nulls were bad but didn't know how to completely get rid of the null.
I'm thinking that Robs initial notions of sum types and parametric polymorphism is that they can only be implemented via hierarchies of inheritance which in itself has many problems. It makes sense because this is what systems programmers are exposed to (C, C++) as opposed to typed lambda calculus or Haskell. So it's easy to see that Go is the result of Robs awareness of problems with OOP but lack of awareness of the theoretical alternative.
First, it's often possible for the function to return a meaningful value even in an error case (e.g., number of bytes read before the error occurred).
Second, it's often possible to return a sensible 'null' value together with an error which can be handled correctly without checking the error value. (A map lookup is the obvious example of this.) This simplifies logic in some places.
Using sum types for errors in Go wouldn't actually work very well unless you fundamentally changed other aspects of the language. You'd need pattern matching, a whole bunch of generic higher order functions for manipulating option/result types, etc. etc.
Create a type that explicitly stores this information. The return type can hold the (error message and a value) OR (just a value). This type expression is a more accurate description of what's really going on. Whichever way you want to represent the product type it's not isomorphic to the actual intended result that the function should return. A sum type can represent the return value of the sentence below while GO cannot:
"A function that returns (a value) OR an (error with a value)"
This is the true intention of the function you described.
>Second, it's often possible to return a sensible 'null' value together with an error which can be handled correctly without checking the error value. (A map lookup is the obvious example of this.) This simplifies logic in some places.
But opens up the possibility of a runtime error if you fail to check for it. Historically there are tons of functions in C, C++ or javascript that use null to represent an error and it is self quoted to be the greatest mistake ever made by the creator of null. No language needs a null.
>Using sum types for errors in Go wouldn't actually work very well unless you fundamentally changed other aspects of the language. You'd need pattern matching....
Using product types to represent errors has already changed the nature of GO in a very hacky way.
Only Functions in GO can return tuples and the concept of the tuple can never be used anywhere else. You cannot save a variable as a tuple, you cannot pass a tuple as an argument. You can only return a tuple then instantly unroll it. It's an arbitrary hacky feature obviously made to support error values.
It would be better to have an arbitrary pattern matching feature... this makes more sense then arbitrary tuples returned from functions.
>a whole bunch of generic higher order functions for manipulating option/result types, etc. etc.
Actually no you don't. The fact that go functions return tuples with errors, does this mean that higher order functions need to handle tuples? No! not at all. In fact go explicitly eliminates support for this... The tuples in GO need to be unrolled into their constituent types before they can be used in any other function. The same concept can be applied to Option types. You have to unroll the value and explicitly handle either individual type. You do not ever need a higher order function that accepts the Option type as a parameter.
Like all languages that have the Option Type/Maybe Monad etc... Any function that returns this type represents a function that is impure that needs to be unrolled first before passing the value down to the functions that do closed and pure calculations. A function that takes an Option type as a parameter is a function that says "I am a function that can only take values from IO" It's very rare for functions to be implemented like this even in languages that have first class support for the sum type and Monads. In haskell I can't recall ever seeing a function that takes the IO monad as a parameter. In haskell and in Rust these values need to be unrolled into their constituent types before they can be used.
Please note I am not advocating the inclusion of Monads into GO. Just talking about sum types.
The fact is that Rob almost certainly knows more than you, rather than less. And he still made the choices he made. That should make you ask questions, not about Rob's knowledge, but about yours.
1. Rob Pike spent all that time at Bell Labs, with all these CS experts, read and wrote all those papers, and never heard about sum types. That's... possible. It's not the way I would bet, but it's possible.
2. Rob Pike knew perfectly well what sum types were, and left them out of Go, because he thought they didn't fit with what he was trying to do.
To me, the second is both more charitable, and more in line with what I think Rob Pike's background and experience would have exposed him to. crimsonalucard obviously disagrees. He seems to think that sum types are so obviously the right thing that Pike could not have possibly not put them in Go had he known about them, and therefore he could not have known. And that is in fact possible.
But it seems to me to better fit with Pike's background, as well as with the principle of charity, to assume that he knew. And still he chose to leave them out.
Now, he could still be wrong. And we can discuss whether sum types are really a good fit for what Go is trying to do. But the assumption that he couldn't have known, or he would have done it the way someone else thinks he should have, is what grates on me.
For what it's worth, the Go FAQ (at https://golang.org/doc/faq#variant_types) says that they considered sum types, and didn't think they fit.
Please read my initial assumption. In no place did I say he COULDN'T have known. Read it. I literally started the statement with "I think the designers probably didn't know" rather than "I know they COULDN'T have known." There is nothing to "grate" you here. I simply had an opinion and a guess, and you disagreed with it and decided to insult me.
What grates me is the assumption that I said it's 100% true that Rob Pike didn't know what a sum type was. I think it's very likely he didn't know. If he did know then I am wrong. That's all.
>For what it's worth, the Go FAQ (at https://golang.org/doc/faq#variant_types) says that they considered sum types, and didn't think they fit.
That FAQ should have been presented earlier in a cordial and civil way. If you did I would have admitted that my hypothesis was incorrect. Science logic and evidence rule the day and I try to not invest any emotion into any of my opinions. It's hard but I follow this rule. If the FAQ says he knows about it then he does and I am wrong. Instead you chose not to present this evidence and call me arrogant.
There was no need to call me "Arrogant." It disgusts me to hear people talk like this. Either way the GO the language feels awkward in the way it uses product types and does indeed feel like Rob didn't know about them because the sum types certainly do feel more fitting then having a function return a tuple out of nowhere.
I also disagree with the FAQ. Plenty of languages have constraint types that are placed on the subtypes of the sum type. There's no confusion imo. Also note that the previous sentence was just an opinion. Please don't call me arrogant because I have one.
And I don't see the FAQ as necessarily total vindication of my position. The language team considered sum types; it doesn't mean that Rob Pike did in the initial design. It could be that, after it was kind of mostly formed, they thought about sum types and couldn't find a sensible way to make them fit. Or it could mean that he considered them and rejected them from the beginning. The FAQ isn't specific enough to say.
As for calling you arrogant: You are not the first person who has said, here on HN, that Pike "looked like he didn't know"/"must not have known"/"couldn't have known". Those conversations kind of run together in my mind. As a result, I was hard on you at least in part because others went too far. That's not fair to you, and I apologize.
I also cannot call you arrogant for having an opinion. I also have one - you may have noticed this. ;-)
However, I feel that I should say (and say as gently as I can) that you often sound very harsh on HN. A harsh tone causes many to read your content with less charity than the ideas might deserve. (I am not here trying to defend my interaction in this thread.) And this is not very helpful of me, because if you ask for advise on what, specifically, to change, I'm not sure I can give any. I mention it because you may be unaware of it, and awareness may help.
I can easily see how the previous paragraph could offend you. I am not trying to do so. Forgive me if it causes offense.
I have been using the Wirth languages a lot (Pascal, Modula), and one of the big appeals of Go to me is, that it brings back a lot from those languages to the modern times. The Wirth languages are far to underrated in programming today.
When Go came out, I though it could follow Oberon, starting small and eventually reach Active Oberon/Zonnon expressiveness, but alas that is not how they see it.
Even Limbo has features that Go still lacks.
More charitable to Rob Pike, rather than to the person you're in the the middle of a conversation with.
Anyway "appeal to authority" is not an argument, it's a religion. Our lord and savior, Rob, knows so much that his design decisions are beyond question.
Read that quote by Rob and the responses. IN the quote rob describes what he believes Generic Types are at face value... he literally takes it into a tirade about inheritance and hierarchies of classes... something that is not part of type theory at all.
Honestly, it feels like he didn't know about Algebraic Data types. I'm not the only one who thinks this as shown from the responses to his quotation.
One of the responses:
"Or perhaps Rob Pike just hasn't explored the relevant literature in enough depth. At one point he admitted that he didn't know that structural typing had already been invented previously! This isn't to criticize Rob, I find his talks fascinating, I think he's awesome, he's a friend of my boss, etc. But he's hardly the first hard-core hacker to be ignorant of the degree to which type theory has seen dramatic advances since the 1980s."
So, at the point they were creating Go, I think it's perfectly reasonable they had even less exposure to fp, and didn't actually know about these better solutions.
Firstly, "enums" using iota should always be defined as either
const (
Red Colour = iota + 1
Blue
Green
)
or const (
Unknown Colour = iota
Red
Green
Blue
)
to avoid the zero value problem.Secondly, and this is a personal preference, I've really enjoyed not having to work with sum types. In practice other programmers seem to use them when a public interface would have been sufficient, and it's convenient to be able to do:
// original package
type Position struct {
X, Y int
Rot Rotation
}
type Action interface {
Apply(Position)
}
type MoveForward struct {
Steps int
}
func (m MoveForward) Apply(p Position) {
switch p.Rot {
case Up:
p.Y += m.Steps
...
}
}
// second package wrapping the first
type WarpToPoint struct {
X, Y int
}
func (w WarpToPoint) Apply(p movement.Position) {
p.X, p.Y = w.X, w.Y
}IME it’s useful to explicitly define the zero value’s meaning as “UNSPECIFIED”, to simplify the problem of trying to guess if the client intended to pass a zero value.
Because there's not actually support for enums in go.
There's support for constant values, and automatically assigning sequential values to them. That happens to be useful for solving the same kinds of problems that enums solve, but they're not equivalent.
Your example of what you prefer uses (a shitty approximation of) a sum type in p.Rot.
(This is also the most basic possible use of a sum type; they are not only useful for enums, it's just to point out that even a large amount of "simple" Go code would benefit from them.)
I want to reiterate that I am aware sum types can be useful. I just don't think they're useful _enough_ to outweigh being a footgun for calling code.
For example, consider an AST for a programming language. AST nodes don't have any behaviour (though they might have some methods for convenience, though nothing that implements behaviour). You want to do optimizations and printing and compilation and so on, but on top of the pure AST.
That's usually sufficient. Any "cheating" should come up in code review as a suspicious cast to Color. In an audit, you could search for casts to Color.
Safe languages often have unsafe constructs. It's the same principle. The unsafe code is signposted, and you review it.
If you want further encapsulation, another useful trick is to make Color a struct with a private field. It's not usually necessary, though.
Go does have an unfortunate quirk that you can always create a zero value without calling a constructor, so you'll need to make sure a zero Color has meaning. (An interface doesn't really change this because the then the zero value is nil. That's not an improvement over making the zero value mean "black" or "transparent" or "invalid".)
type Color struct {
R, G, B, A byte // IDK
}
func (o OtherType) *Color {
return &Color{R: o.r, B: o.b, G: o.g}
}
type Colorer interface {
func Color() *Color
}
A colorer would return a color regardless of its type. This is behavior-based interface. I just need a thing that when I call Color(), you get a *Color.github.com/go-playground/validator/v10
It uses struct tags to validate the struct and is quite extensive.
¹ Yes, even over correctness.
How easily can people understand what this code does and how?
This is at least conceptually objectively measurable, though I don't know of any actual attempts to do so.
That's why things like C coding style is so variable - sometimes within the same body of code (see net-snmp ....)
I'd rather have consistent coding standards but I daily deal with different team projects with different conventions, so I'm used to adapting my own reading conventions as I switch contexts.
Keep a common aesthetic within a project. It's worth it - by measure of success of a project.
The argument that "all code gets sloppy so let's just have very verbose code from the get-go" is pretty insane to me.
Correctness is a basic starting point. It's the minimum viable offering.
Here is my counter argument.
Readable code that has some bugs is fixable. Because you can understand what it's doing and how to change it.
Working code that is unreadable is basically dead. No one can make changes to code they don't comprehend. The only thing it's good for is running it as is, much like a compiled binary.
You don't see people in Go doing this, and it gives the impression that the community is small, but I think they're just building stuff.
Sometimes, these connections can be unclear from the outside. For example, there’s a lot of talk about “generic type constructors” and “generic associated types”, which sounds academic. However, it’s something the compiler needs to understand in order to implement a very simple user-facing feature: async functions in traits. From the outside it may look like “oh those folks are out of touch” but it’s directly connected to real user needs.
(As a further aside, these two features are identical, but “generic type constructors” focuses on the academic, and “generic associated types” focuses on the end-user benefit; we changed out terminology here specifically to focus on what users get out of the feature rather than the type theory implications.)
Furthermore, some people tweeting about things they’re excited about does also not preclude others who are heads down all the time. You wouldn’t see them for the same reasons, they’re not tweeting.
These kinds of swipes against other languages lower the discourse and promote animosity when there really should be none. I’d encourage you to consider if these kinds of attitudes help bring about more people who are interested in building cool things, or fan flame wars that distract folks from doing exactly that. Every minute spent arguing over whose language is better is also a distraction from building cool things as well.
We rejected a proposal for dependent/pi types; we’re still adding a limited form, and may get there someday, but we didn’t want to go fully into them at first because the difficulty was high, and the benefit less clear, than just the simple version. (Const generics)
There’s a few other other features that we had and removed too I can think of off the top of my head. We used to use conditions for error handling. We had typestate.
There was a battle over type classes vs ML style modules, type classes (traits) won in the end. That doesn’t mean modules are useless...
I think the answers to these questions are very relative to your langauge’s values and goals. All of these features have good uses in other languages, but couldn’t find a place in Rust for a variety of reasons. Your language should be different than Rust, so you may find some of these features useful, and not find some of ours useful.
I would encourage you to read TAPL, I think it would help with what you’re trying to do. Oh and check out https://plzoo.andrej.com/
Not always, some Rust community members in fact don't build any sort of applications with Rust at all, and only build libraries. I think it's reasonable to be skeptical of this.
I think the community can do better by promoting more talks involving applications written with Rust. As an outsider this is a puzzling omission to me as there seems to be more people using Rust than there is Firefox and Cloudfront engineers, so I'd like to learn more about where it's actually used.
This is obviously false. How do you square that with all the Rust code we've shipped in Firefox, for example? I build things in Rust every day.
>> This is obviously false. How do you square that with all the Rust code we've shipped in Firefox, for example? I build things in Rust every day. - pcwalton
> I didn't state that no one builds anything in Rust so I have no need to square it. - Touche
Touche!
You are technically correct I suppose. You didn't state no one builds anything, but arousing suspicion that "a lot" of prominent Rust people aren't building anything and are just "snacking" on the language is pretty pointed rhetoric with an obvious purpose.
You could start up a Programming Language tabloid with a headline like:
EXPOSED: PROMINENT RUST PROGRAMMERS CAN'T EVEN WRITE IN RUST!
And really that's all before analyzing the line of logic of "people tweeting only about interesting language features and not their personal projects, public work projects, or private work projects implies they might just not be building anything at all" which seems pretty flimsy at best.
There was a thread on the Rust reddit where someone was asking how to do something relatively simple using some elaborate combination of map/reduce/filter/continuations/who-knows-what, and someone said "just use a for loop", and the OP was enlightened.
People don't know how great the burden of trying to model their problem to fit a fancy language is until it's gone. I didn't.
I want generics and sum types, but I miss them less than I would have predicted.
I think this is a process of learning. While learning a new tool you start to model problems so you can practice, even though not necessary. Once comfortable you realize when to use the tool and when not.
(Also, rust’s for loops are implemented in terms of iterators; they’re actually the more primitive construction in a language sense; a while loop with the Iterator library API.)
I also write Go code without missing generics, but that’s because I’m also fluent in other languages, and tend to use those when I want something ill-suited to Go, rather than trying to force Go into that shape.
I think this should be the main takeaway from people learning go - it's not suited for everything. Technically you can write "World of Warcraft" in pure assembly, but it doesn't make sense to do - you're using the wrong tool for the job. My problem is I hear a lot of people advocating for golang with a one-size-fits-all, theres-nothing-better, sort of mantra.
I have things I absolutely reach to golang for, but the sweet spot I've found is to re-implement a prototype I've built in some other language (like Python) when I need the speed. Trying to actually create new things in golang is tedious and I end up fighting the tooling more than most other languages (sans maybe C++ or Java).
Pun intended? If so, nicely done.
I think it's hard to discern between "that overly-complex functional and declarative definition is unfamiliar to me" and "that is way over-complicated and should just use a for loop".
Any chance you can track down that example so others can compare the two examples as well?
It is highly stimulating intellectual effort though. Sometimes I sit down and spend hours just thinking about the best way to do something. It's some kind of philosophy: the abstractions we create reflect the way we understand things. To write good code, we must study the computer science and the problem domain itself.
Without this, it's just boring mechanical work. Once the project has been figured out it ceases to be interesting. Some of my projects are unfinished because I can't justify spending more time on them even though I know exactly what must be done.
""" The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt. """
I read that as Rob Pike saying he wrote go for idiots google hires to write.
https://bravenewgeek.com/go-is-unapologetically-flawed-heres...
I rather disagree that making your code readable and maintainable is a waste of effort.
Saying your code is going to "look like sh£t anyway" seems rather defeatist, and an _excuse_ to write unreadable code.
There are some parts of the language that are such a joy to use – C interop is simple and elegant, the concurrency story is great, the standard library is great, and in contrast to some other people, I think the error handling is also a nice example of simple and effective design. The wider tooling system is decently usable (with the exception of gofmt, as mentioned in the article, which I think is the single best tool I've ever used in any language).
But "simplicity" in Go-land seems sometimes to be the enemy of expressiveness. The lack of abstractions over operations like filtering, mapping, or removing items from a collection is incredibly irritating for such a common operation – instead of immediately obvious actions, there are tedious manual loops where I end up having to read like 10 lines of code to figure out "oh this just removes something from a list". The use of zero values is a crime against nature. The type system is shameful in practice compared to other modern languages, with so much code just being "just slap an interface{} in there and we can all pretend this isn't a disaster waiting to happen". It feels like such a lost opportunity to exploit a richer type system to eliminate whole classes of common errors (which are predictably the ones I keep making in Go code.)
I guess it's frustrating that a language—which is relatively well-designed and intended to be simple—too often makes things more complicated overall by making fallible humans do things that computers are better and more reliable at. I'll keep using it, because it fills a really nice niche that no other language is quite suitable for. But I'll keep being a bit annoyed about it, too.
> The use of zero values is a crime against nature
Can you elaborate?
Json in go cannot be represented as a type and also is not type safe. This is 100 percent because go is missing a basic type primitive.
Json contrary to what many people think is not untyped. Json is represented by a recursive sum type which go has no ability to represent.
I think what caused this issue is more because the designers of go are more systems people rather than language theorists.
Not a Go programmer, but why don't you simply use an explicit type tag and a number of getters? They can assert that they're not called in invalid situations.
The worst offender is `Time` – to quote from the documentation:
"The zero value of type Time is January 1, year 1, 00:00:00.000000000 UTC. As this time is unlikely to come up in practice, the IsZero method gives a simple way of detecting a time that has not been initialized explicitly."
That seems to be an absolutely baffling decision.
Without some kind of default value, you end up with a lot of nested constructors and loops initializing array elements and fields, versus just zeroed out memory.
C++ does the constructor thing but it seems complicated and finicky when you don't have a zero-arg constructor. When I looked at Rust array initialization it looked somewhat limited, but maybe I missed something.
I'm not sure if every type really needs a zero value like in Go, but it seems like standard types like timestamps should, so you can use them this way?
I also don't see what your problem with time is. Zero is one set point in time. Not sure, why you want an "invalid" timestamp. Does make as little sense to me as an "invalid" integer. Why would you need to check that a time value has been explicitly initialized? And for most purposes, the zero value pretty much would express that anyway. If not, you can wrap the time value into a struct with a nonpublic initialized bool slot. But I really would like to know what your use case for this would be.
Sometimes I run into a situation where I’m like “Sigh, filter would have been nice here.” But it’s pretty rare. On the other hand, ”clever” programmers love to make incomprehensible messes out of functional constructs.
I think this is sometimes why it’s so hard to compare and contrast languages; even surface level features in two different languages can have two very different underlying implementations, which can mean you may like a feature in one language and dislike it in another.
This hints at some of the ideas behind Go – it's designed, perhaps, for Google-scale software. This is dealing with problems (like e.g. memory allocation) that I don't have when working with most datasets I'm likely to need. Maybe we just have to accept that.
This is exactly the kind of thinking that the Go language pushes back against.
> Maybe we just have to accept that.
I think so. At least for now. I recently watched a talk by Ian Lance Taylor that made it very clear to me that generics are coming (https://www.youtube.com/watch?v=WzgLqE-3IhY). When we have generics then map/reducer/filter will absolutely be introduced as a library at the very least.
> This is dealing with problems (like e.g. memory allocation) that I don't have when working with most datasets I'm likely to need.
I don't think that's exactly it. It's more about runaway complexity. You might use these primitives to perform basic operations but other people will misuse them in extreme ways.
Consider this: suppose there was a built-in map() function like append(). Do you use a for loop or a map function? There'll be a performance trade-off. Performance conscious people will always use a for loop. Expressive conscious people will usually use map() unless they're dealing with a large dataset. This will invariably lead to arguments over style among other things.
I know all about the virtues of functional programming patterns, and use them in personal projects, but in my day job working with dozens of engineers in the same codebase, I appreciate not having to decode the idiosyncrasies of how each engineer decides when and how to use higher order constructs, and the subsequent performance, operational, and maintenance implications. It’s a lot easier for me to just read a for-loop and move on with my life.
I did that to typescript, should be applicable to go as well. Ref: https://github.com/beenotung/tsc-macro
C interop in go is super slow: https://github.com/dyu/ffi-overhead
So calling C from Go for small functions isn't a performance gain. You should write those in Go. The calling of C is great for linking against larger libraries, which cannot be ported to Go. And for that it works nicely and is fast enough.
It may be fast enough for you, but it certainly isn't for many other people. golang will for example never grow a large mathy/scientific ecosystem, because of it.
I find I use Go quite often to build relatively compact tools and services that need to use some features of a fully-featured and popular C library to perform some complex function that would be expensive and time-consuming to implement.
A recent example of this is using `libusb` and `libgphoto2` to enumerate DSLR cameras attached over USB and capture images from them in response to some other events, with a small web UI on top. It's maybe a few dozen lines of Go to call some enumeration and capture functions, and then I get a nice byte slice with all the data I want. There is minimal friction, the interaction is clear, and any performance cost is worth paying because of the simplicity of the interaction.
It's entirely true, and a well-known caveat, that the C FFI is slow. This makes it inappropriate for some use-cases, but entirely suitable for others.
One example of Go's lack of expressiveness is that loops are not abstracted away, e.g. you can't use higher level constructs like map and filter on collections. There are other languages that IMO strikes a better balance between simplicity and expressiveness, like Python and Elixir.
1. https://ieeexplore.ieee.org/document/1702967
2. https://link.springer.com/chapter/10.1007/978-1-4471-3003-1_...
3. https://www.amazon.com/gp/product/0735619670/ (Chapter "Developer Testing")
If the lack of expressiveness equals to greater amount of code and thus more bugs (in Go), does the same apply for languages that are more verbose (say, Java)? And if language is more expressive and less verbose (say, Python or Javascript maybe?), does it mean less bugs?
The number of bugs in a program correlates with the number of LoCs, but that's about it (correlation does not imply causation).
The claim is also quite absurd, since, for example, you can pre-process any C or C++ program ever written into a single line of code, yet this operation doesn't reduce the number of bugs in these programs, therefore the amount of bugs is not "just" a function of the amount of LoC.
Languages with better abstraction capabilities than Go (e.g. Rust or Python), might require programmers to type less, but the resulting lines of code are often dense. For example, I'm quite comfortable with Rust iterators and Python lists comprehensions, but it still takes me much longer to parse what code using these abstraction does, than code that just uses a simple for loop. It definitely feels great to cram a 10 LoC loop into a 1 LoC list comprehension in python, but reading that code 6 months later never felt that great, even for code that I've written myself. I just need to stop at those lines for much longer.
No, but it suggests it, and in this case, there's not many other options that could explain this than causation.
>The claim is also quite absurd, since, for example, you can pre-process any C or C++ program ever written into a single line of code, yet this operation doesn't reduce the number of bugs in these programs, therefore the amount of bugs is not "just" a function of the amount of LoC
That's an irrelevant strawman. When people speak of LoC they mean as to how many LoC are needed for various tasks based on the expressiveness of the language. Not whatever old LoC based on arbitrary adding needless lines (e.g. by preprocessing to inline function calls)...
for (dd=d[k=0]; k<16; dd=d[++k])
{
satd += (dd < 0 ? -dd : dd);
}
The problem with for loops is that they are too flexible, and so there's more opportunity for misuse. Iterator functions are less flexible, and therefore easier to read.And this excessive flexibility doesn't even make simple things simple. Try counting down from N to 0 inclusive using unsigned ints.
for k, dd = 0, d[0]; k < 16; k, dd = k + 1, d[k + 1] {
...
}
I don't find that significantly more readable than the C version.I'd like to see how solving this same problem in those languages is less of a nightmare.
I personally find that C loop quite readable compared with the same iteration being done in Rust.
let satd = d.iter().cloned().map(abs).sum();
As an added bonus, there's no undefined behavior!But when looking at OP:
> bugs in software has been shown to be a function of LoCs [1,2,3].
He provided three different sources! Maybe it's wrong, or maybe you don't believe it, maybe it doesn't fit with your beliefs, but you can't say the OP claim is unsubstantiated when he provided academic sources on the subject!
This is an interesting study on programming languages' effect on software quality: https://web.cs.ucdavis.edu/~filkov/papers/lang_github.pdf
I'm not sure it makes sense to consider code correctness separately from productivity. You can probably get code written in any language to roughly similar levels of correctness. But how much work does it take to get there? That's what I'm really interested in.
It illustrates what you have to pay to get this kind of functionality for arbitrary slices.
I wonder how this would change with generics.
For example when should I use NamedTuple and when should I use a dataclass? Should I be using a list comprehension or a map/filter/reduce? When should I split that list comprehension? I have seen (and made myself) terribly long lines with only list comprehension and it was hideous.
yes, use list comprehensions -- they are more readable.
>When should I split that list comprehension?
just use common sense. if you can read it and understand without thinking too much, that's good.
it's readability beats short code 100% of the time.
Besides, in practice I find lines of code in Go vs. Python are only maybe 20% different in many common cases, if you ignore the struct declarations which I have a hard time calling "code". (Being not a Turing complete language, that particular subset can't be considered vulnerable to the issues that Turing complete things can have.) I find myself very frequently wondering how many people complaining about all the lack of abstraction in Go have A: used it and B: used it to the point they're writing Go in Go. (Go just blows if you're trying to write Python in Go or something, or if you insist that you know better and must use map, filter, etc. rather than for loops. For all languages X, writing X in X is important, but Go is really graceless if you don't learn how.)
Now, if you do get to B, it is still true that Go does not quite abstract as "well" as some other languages, but it does abstract well enough that you can start having reasonably conversations about whether those other abstraction tools are actually paying their way in a lot of larger, shared code bases. If we throw out a ton of language complexity, but only pay a relatively small price, are they really carrying their weight? The answer will vary from code base to code base, but I think it's not necessarily some sort of horrid, unthinkably idea that not every program must be assaulted with the most powerful, complicated toolkit regardless of the price incurred by the complication.
The results in those "past decades" used all kinds of languages, some far more advanced and modern than Go is today (e.g. Lisp, Smalltalk, Ada).
In a sense, Lisp is the most advanced language ever designed, and still, it is ignored by most of the programming community :). Equally, I think, Smalltalk does object orientation better than most languages which followed it.
Both languages are mostly dynamically typed, so comparisons to static typed languages need to take this into respect, they are different beasts. On the other side, I think Go has an interesting mix out of static and dynamic typed features, having interfaces for static contracts and interface{} as a fully dynamic type, which still offers full type safety at runtime.
Go structs have also some interesting properties, if you look at them beyond being containers. Any type can have methods, and structs can embed other structs and thus "inhert" their methods in a sense. This is not full blown object orientation, but gets you surprisingly far.
The Python version is around 1/3-1/2 the size of the Go version, but the clarity with which bugs are visible in the Go version is astonishing.
There's a lot of code in Python version that "looks right" and even "does kind of the right thing a lot of the time." But the Go version lays bare corner and edge cases that were not obvious in the concise Python version.
I agree that Go usage does suffer sometimes from lack of generics. I've sometimes used code generation in its place, and it works, but it's a poor solution. At the same time, the lack of generics has helped me avoid over-abstracting in other places, which is a common bane of mine whenever I work in more "expressive" languages.
This is (or was) deliberate, with the rationale being that it makes O(n) blocks of code trivial to identify in review.
Whether or not you buy that argument is a separate thing, of course.
Whereas, greping for .map(), .filter(), .reduce() wouldn't?
Like how
for (int i = 0; i < strlen(str); i++) { ... }
is O(n)? for (int i = 0; i < strlen(str); i++) { ... }
could be grepped and easily discerned from: for (int i = 0; i < str_length; i++) { ... }
and generally, that every looping point could be easily identified.After all, if you call functions inside the body, all bets are off as to what complexity the loop means anyway...
How is such identification made easier by manually writing out a `for` loop applying a function foo to each element rather than writing `map foo myCollection`?
Would anyone expect such a developer to read and parse several more lines of code more reliably, in order to understand the algorithmic complexity? Seems unwarranted to me...
In programming, if you miss an important detail the repercussions can be high. In a million line codebase every idiom that's slightly more complex than it needs to be is going to result in dozens of additional bugs, sheerly because of the increased surface area for making mistakes with that idiom.
I think the clue to your thinking, for me, is in your description of the `map` HOF as "slightly more complex". Having years of experience with both paradigms, I've found that grokking a call to one of these fundamental building blocks (map, filter, reduce, fold, etc) is nearly instantaneous. We've all experienced reading prose where the author was excessively verbose when the same point could have been made succinctly. It feels the same way reading for loops once you get over the learning curve of these very basic functional constructs. You have to keep repeating that boilerplate endlessly and it's very tedious to keep writing and reading it.
Yeah, and it's much easier to make mistake with a loop than with a map or filter.
Memory allocation is so expensive that making that copy is often more expensive that calling `foo` on each element.
Sometimes making a copy is exactly what you need and there's no way around that cost (but hold that thought).
But I've also seen `sum map foo myCollection` so many times (especially in JavaScript).
Here you have a short, neat but also extremely wasteful way of doing things. I see it so frequently that I assume that many people are unaware of this cost (or completely disregard performance concerns).
If you were to write this imperatively, it would be obvious that you're making a copy and maybe you would stop and re-think your approach.
But there's more.
If you're paying attention to performance, an easy way to improve performance of `map foo myCollection` in e.g. Go is to pre-allocate the resulting array.
For large arrays, this avoid re-allocating of underlying memory over and over again, which is the best a `map` can do.
In imperative code those costs are more visible. When you have to type that code that does memory allocation, you suddenly realize that the equivalent of `map` is extremely wasteful.
Language support for Transducers can fix this, you can compose functions like map / filter / reduce over a collection and only hit each item once.
numbers.iter().map(|n| { ... }).sum();
Compiles to a plain loop, with no allocation.Your points about efficiency are a separate topic entirely from the original claim that manually writing out an imperative solution makes it easier to see the algorithmic complexity. That was a surprising claim to me because, in my experience, if I understand what some HOF is doing, reading the code is even easier because there is less of it to wade through (and mental exhaustion doesn't make one easier to read vs the other).
> In every mainstream language I can think of `map foo myCollection` creates an intermediary map
You need to build up the final result, sure. Not an intermediary map but whatever structure (or functor) you're mapping over. That is the whole point of immutable data structures. Also when you're using persistent data structures, which all modern FP languages do, the cost of constructing the result can be far less than what you expect, especially if the result is lazy and you need only some of the results. There is a cost to immutability and if it's unbearable in some situation, fall back to in-place mutation but the semantics of these two approaches are definitely not the same.
> But I've also seen `sum map foo myCollection` so many times
Yeah... that should be a fold (reduce, whatever). :)
That said, anecdotally I too have found it much easier to find errors visually in go programs due to its simplicity. I find the same is true of C vs. C++.
> Code duplication is more acceptable in the Go community than elsewhere.
> Many times, you’ll need to write that functionality yourself, or copy/paste it from a StackOverflow answer.
Am I the only person that this is regression? How can code-duplication be defended by 'simplicity'?
> Fewer dependencies means fewer things that break while you let a project sit idle for a couple months.
I thought that pinning dependencies is \the\ solution for this? I don't use Go, so I'm not sure about the Go packaging story, but is there no package pinning solution? Yarn & npm handles this beautifully...
Edit: not sure why the downvotes, perhaps these people have never seen a junior engineer go on a fanatical deduplication spree across a whole project, or perhaps these are the junior engineers.
I’ve seen people link to multi-megabyte modules to save one line of duplicated code. At Google this was a disease. Good to see they’re fixing it.
Most code is trivial.
The question we should ask is whether or A should change whenever B changes. If the answer is yes then there should be a dependency. If the answer is no then no dependency should exist, even if some lines of code happen to be identical at a particular point in time. (That's probably what you alluded to when you said "unrelated pieces of code" and I agree with that).
The number of lines of code shouldn't be the primary concern though.
For instance, if some tax is calculated in a particular way as a matter of law, then that calculation should exist exactly once in the entire code base, even if it's just one line of code.
Spot on.
Avoid applying DRY principles to incidental duplication.
It can be easier to maintain a small amount of code that you duplicate and check in and is purpose-built to your use case and treat as your own, compared to a general library imported for one routine that you have to think about (licensing, upgrading, auditing). Or sometimes libraries break backwards compatibility to add features you aren't interested in, and it makes busy work to stay up to date.
> Is there no package pinning solution?
Multiple dependency managers have supported pinning since at least 5 years ago e.g. glock, dep, glide. Go Modules supports pinning.
> Pinning is the solution for avoiding breakage due to dependencies
Related to the first point, in general you have to do work to upgrade your dependencies. If not because you care about bug / security fixes that happen, then because you use ANOTHER library that has the same dependency, and that other library uses a feature only present in a newer version. Any time you upgrade a library it takes work to verify nothing has broken, and potentially work to fix things that do break.
I think that might be a difference for folks coming from other ecosystems.
[1] The asterisk is that you can have multiple major versions coexist using Go modules, or you can use gopkg.in import paths, or some other workarounds, but in my experience that is not common and typically you do have to find a single version that works for your whole program.
My experiences trying Go Modules has shown it to “randomly” modify the go.mod file.
Yeah I love DRY and code reuse as much as the next guy, but sometimes copypasta makes sense.
I will say go's nature and/or culture make grafting in functions a lot easier.
> compared to a general library imported for one routine that you have to think about
IMHO, that's why small, one-utility-function libraries in the JS ecosystem([0], [1], [2]) are useful. (Unlike the general tone in HN where npm and left-pad gets a lot of hate.)
Look at Quickutil[3] for examples of other languages. (In this case, Common Lisp.)
[0] https://github.com/sindresorhus/mem/blob/master/index.js#L7
[1] https://github.com/sindresorhus/tempy/blob/master/index.js#L...
The overhead you're talking about isn't a real problem in my experience, as long as your dependency manager has semver support (all of those I know).
When writing (or copying from SO) a simple snippet that's supposed to do just what I want, almost 80% of the time there will be a bug in it (typically edge cases are not covered)…
For instance, before Go 1.10, there was no rounding function in Go's std lib, and 99% of the code you could find on the internet to do this (simple) task was buggy: some failed with negative numbers, and others due to bad handling of floating point numbers…
The code is hard to follow and modify later if the common functionality needs to be handled differently.
There are many times when working on my old code that I was burned by this.
Code duplication isn't necessarily a good thing, but the cure can be worse than the disease. It is better to be pragmatic.
Obviously trivial cases of code duplication could/should be resolved, but for less than trivial cases sometimes its just fine to cmd-c & cmd-v with some minor use-driven modifications.
Remember the lib versions of many of these snippets need many many parameters to satisfy everyone's needs.
Look at our fiction and non-fiction writing; very few real plot loops and themes. There’s a common wisdom in writing I’ve heard called “kill your darlings”, those bits of prose that you love in isolation, but big picture, really don’t add to the detail of the world or motivation of the character
I do the same with code: keep code itself vanilla af to avoid complicating or distracting from the big picture of its value
Oh and that’s at work. To be certain, I fiddle with favored abstractions and ideas on personal projects when the mood strikes. I cram assumptions into abstractions because it’s just me.
But at my job, where I only go because of social pressure to conformity, I don’t really want to wade through other people’s emotional opinions. I just want to copy/paste together same old to keep it easy.
Give a programmer a job building some thing real like a car and you’ll get The Homer.
However go is missing fundamental concepts of algebraic data types that in itself is causing problems. In their effort to create a language that moved away from the flaws of OOP they failed to have knowledge about type theory and created a language that in removing the flaws of OOP they have also accidentally removed fundamental language concepts. My guess is that they didn't know about the theoretical concept of sum types at all because sum types exist in Java as inheritance.
The two biggest being parametric polymorphism and sum types and it is these two things and the consequences of these two things that are causing the ugliness in go. Everything from default zeros, functions that return both errors and values and repeated code.
Easily. People can avoid code-duplication in many ways, and some of them are harmful.
E.g. by building abstractions that get out of hand and add mental burden to track the program flow and bugs.
Or by making any small piece of code into a standalone 2-3 line function, so that anything always happens elsewhere, and keeping track of flow is made difficult.
Or by prematurely putting code into a function when it's just used in 2-3 places, and then when another need arises that doesn't fit in to the already captured function, hacking the function to handle different behaviour on different arguments/flags and ending up with monstrosities.
This is just an anecdote, but recently I decided to give golang a spin, and wrote a small command line utility in it.
I won’t tell you the pain of trying to find an argument parser with support for GNU-style long options. I suppose that’s something I should write myself rather than use the standard library or find a third party library, because simplicity. /s Edit: folks posted helpful suggestions, I take this paragraph back.
Instead, let’s talk about something basic, regexp. Turns out golang’s regexp package supports named capture groups, but if you want to actually refer to the capture groups by names, you need to manually pair FindStringSubmatch result with SubexpNames in a loop. Can’t make this shit up. Seriously, what’s the point of named capture groups if you can’t refer to the groups by names? If this is just enough battery included, your expectations might be a tad too low.
Besides, isn’t it feasible someone might confuse long and short options? “Is -d and —delete the same, or is -d —debug??” KISS even if it’s annoying.
As for loops, I know that they’re generally more common in Go. Another example is finding if an element exists in a slice. Most of the implementations is this stuff is just a loop under the hood in the end.
From a Java background it was strange to me because I’m used to dealing with classes and there’s always a method for this or that...
Users of a command line utility ultimately don’t care what language it’s written in; having a familiar interface is a much more important goal. Since the vast majority of Linux utilities these days use GNU-style rather than Unix-style long options, it stands to reason that the latter is a better choice for users. (There are also technical advantages I won’t get into.) I know Rob Pike probably isn’t a fan given his background, but why not give developers the choice to build what they want to build, instead of shoehorning them into something they dislike? What’s the point of being opinionated here? (Edit: Sorry, didn’t mean to imply that the standard library ought to have it; however, I do find it strange that in the ten years of golang the community didn’t bother to come up with a reasonably good library for it.)
Btw, flags is very primitive and can hardly handle anything complex, even if you accept the Unix-style.
Yes.
> and therefore flags not allowing concatenating short options (e.g. `-r -x` can't be written `-rx`)?
Depending on the implementation concatenation of short options may be allowed; it certainly could be ambiguous. I'm not sure about flags (the golang package) though.
It is a drop in replacement for the built in flag library.
I ususally start out with the built in flag library and migrate to pflag when things get a bit more complicated.
I’ve had the displeasure of working on a pretty complicated Go project at work and it’s given me plenty of counters to what OP is saying.
First, Go’s dependency management has been terrible and inconsistent for years. Dependencies started very loose and without any versioning at all and I was bitten many times by this mostly because developers used the looseness of Go get to consume repositories that were never meant to be public APIs and without versioning those repositories had zero guarantees of compatibility.
The community eventually noticed this was a problem and vendoring was introduced but the community and Go itself flip flopped on a “correct” solution for ages which ultimately led to even more package incompatibility. Now we’ve got modules which is a step in the right direction but it’s going to take quite some time for all of the Go projects to implement it correctly. Modules have been nothing but a pain in the arse for us because our Go project has a lot of dependencies.
Another thing I hate is the idea that people spread that Go’s simplicity leads all developers to write simple, clean code. Some developers, I’ve noticed, pine for abstraction like moths towards a lightbulb. In Go abstraction can be very painful to do elegantly and I’ve seen some nightmares where developers have tried. Using reflection on interfaces produces some of the shittiest code I’ve seen in a long time and yet I’ve seen it repeatedly by different developers in different organizations.
I must not be the only one who has noticed these issues because the creator of Go himself made the Go Proverbs (https://go-proverbs.github.io/) which include things like “interface is never clear” and “reflection is never clear”.
Okay, I’m going to stop ranting now but I seriously don’t get the hard-on for Go. I think I kinda got it before I used it professionally.
But, one day you might wake up and realize it was all a fantasy. That is, if you ever step foot into a shop where you are expected to build a line-of-business application with sufficiently-complex business entities and rules. All of your ideas about how wonderful Go is will fly directly out the window. Sure, you can eventually implement anything in Go. It is Turing complete after all. But, that isn't to say you'll have enough time to implement it. Go advocates will happily dismiss the value of things like LINQ, generics & reflection until they are faced with a reality where tools like these are the most productive way get a feature into production before 2020 hits. If you open your mind up to the possibility of using different (god forbid, unique) approaches to solve problems, you might then find that Go does not even offer the best implementation of these alternative language features.
Go certainly isn't a general all-around fits-all tool, it's a rather specific and opinionated approach. It's almost a given than Go alone can't solve 100% of most company's needs, this is not Js or Python or C#.
So trying to shoehorn a classic OOP or functional paradigm into Go is just asking for trouble, and no the language won't change to become a clone of another (what would be the point?)
If generics and reflection are core to your solution, there are languages for that. Don't make your life harder. Not every codebase needs to be in one language only.
If anything, I've found shipping code to be WAY faster in Go than Java due to the much cleaner build system and approach to testing.
The one main area where Go struggles is in math-heavy code where the lack of generics or algebraic types means duplication or code-gen.
But you're right. It's not the be all end all of languages. If anything, it's just a much better substitute for the use cases where Java traditionally excels.
I have ported stuff from Java to Go, for my long running app, and very happy.
Also at a client place, over the past year, I developed stuff in Python and Go, where Java was traditionally chosen, and it led to faster execution times, and stable microservices.
Take a look at this (short) page which describes ALL the syntax of Clojure (except macros) if you're interested: https://clojure.org/guides/learn/syntax
I think Clojure has a lot of syntax. The syntax is just embedded in a number of conventions, macro mini-languages, and the reader syntax itself. Because Clojure’s syntax can be extended in user space (via macros and related tooling), the syntax also grows more rapidly than other communities.
As a professional programmer who came from Python, Java, C, and JavaScript, I found a lot about Clojure compelling, but “minimal syntax” was not one of the compelling points. To the contrary, I think there is a lot more to learn in Clojure than other languages about how to properly structure your code.
As I wrote in my blog post comparing Python and Clojure:
> ... the Python programmer will observe that in the Clojure program, many aspects of the program are implied, rather than annotated by special syntax. Many Clojure proponents will say that Clojure has a “simple syntax”, but I think this is misleading. They don’t mean simple as in “easy to read without prior context”. They mean simple as in “unceremonious”. Perhaps they mean simple as a contrast to “baroque” (how you might describe Java or C++’s syntax). Clojure does have a syntax, but it is implicit from the layout of the code in the list data structures.
(defn calculate* []
(->> (range 10)
(filter odd? ,,,)
(map #(* % %) ,,,)
(reduce + ,,,)))
This is nominally a library as it can be implemented via the language primitives. But in practice this occupies the same space as Haskell's do-notation, and the learner cannot ignore it. The lack of special syntax becomes an implementation detail.So any given piece of code is very explicit and self-contained. Apart from function calls, you can tell exactly what it does without looking up anything else, like a macro definition for example. It similarly doesn't have a lot of different ways to do the same thing tends to be opinionated about idioms (gofmt, for example).
Something like a Lisp DSL would be antithetical to the Go model of what simplicity means.
It sounds like you’re splitting hairs here. In my experience, macros aren’t any more mysterious than functions. The only difference, after all, is when they’re evaluated.
Maybe they should do away with the abstraction that memory is endless.
I assume that people are trying to attach it to the term “simple” because that word has a much more positive connotation, but the meaning isn’t the same.
Macros, like nearly anything else, are an abstraction that requires a little bit of restraint and common sense when you're using them. The answer to the "functions can have misleading names and therefore can be used to create intractable tangles of incomprehensible code" is "well then don't fucking do that", which is the same answer to the majority of problems people think macros cause.
Lisp code can be arbitrarily simpler to read due to nice macros.
> This is frequently blamed for Lisp's continuing failure to take off.
This is largely a meme spread by people who have never developed in Lisp.
The relative lack of popularity is discussed in Lisp communities, but that particular hypothesis isn't taken seriously, if it comes up at all.
It also comes from the standard libraries which cuts down on libraries you have to use.
For most of the stuff I do with Go I don't need extra libraries and when I use them, there are no suprises which mean I can easily check what they do.
Compare that to JS or even Python, where I have to either use libraries for even the most basic stuff or have a huge amount of syntax and keywords.
Besides that, it also sets the standard for the 'right' way of designing your interfaces and whatnot. In Go, the third-party libraries are often compatible with the standard library methods and interfaces, which is just fantastic in both mobility between libraries, and having consistency with different projects.
in python, you don't have to. same for javascript, before the whole jquery era, and the nodejs era that followed, we didn't even know what javascript libraries were.
Since you mention Clojure: That's useless for me, because it runs on the JVM. One reason to use Go is exactly that it produces static, self-contained executables that don't require any heavy infrastructure.
for example, i couldn't get a library to work the other day because of one of the dependencies needed a specific version of another library but "go get" (1.13.4) was getting utterly confused.
i never looked into the modules thing and i found that there is a 4 parts series of articles on the official blog explaining modules are and why we need it. i just gave up at that point and decided to manually copy the version i wanted to the $GOHOME/src folder and it got me going...
The latter are simple in that they're built around a few core concepts which are available to the developer, almost any tool the language designer had is available to the end user. They're simple in the sense that old school legos or meccanos are simple.
Go is simple in the sense of simplistic, it provides a much larger number of restrictive features and most definitely doesn't give end-developers access to the language designer's tooling. It's the simplicity of playmobil or duplo: the developer is provided with much more concrete (and effective in a way) but much less flexible "primitives".
And this is no secret, there's a pretty famous quote from Rob Pike explaining the design and purpose of Go:
> our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt.
Making codebases look uniform and preventing building abstractions (by the language being actively hostile to abstractions which are not built-in) is in line with that.
Imo it's unnecessarily hostile to compare go to playmobile and diplo, and it really encourages people to overlook the genuinely excellent features that go supports.
It's not rare to see people feel offended by others being productive with Go and liking it.
Java/.NET, Python and Ruby? I say bring it on.
In 2010 Pike presented Go as "suitable for writing systems software" [1] such as: web servers, web browsers, compilers, programming tools, IDEs, and OSes ("maybe").
Today Google makes a web browser, and lots of compilers, and programming tools, and an IDE (Android Studio), and at least two OSes (ChromeOS, Android), and Go is not relevant in any of them (excepting Go tooling).
In 2012, Pike was still pitching Go as a C++ replacement, but was dismayed that it was appealing to Python/Ruby programmers. Why don't C++ programmers use Go? He hypothesized [2]:
> C++ programmers don't come to Go because they have fought hard to gain exquisite control of their programming domain, and don't want to surrender any of it. To them, software isn't just about getting the job done, it's about doing it a certain way.
The saltiness was aimed at Google's own C++ engineers, but I honestly don't think that Go was or even is up to reimplementing v8 or Blink.
But today Go really is enormously successful and compelling. It failed at displacing C++ but found its niche. Kudos. But retcon - it is a mainly server-side language because that's where it succeeded, not what it was designed for.
Today Rust is busy displacing C++, so it can be done. Go just wasn't the one to do it.
1: https://web.stanford.edu/class/ee380/Abstracts/100428-pike-s...
2: https://commandcenter.blogspot.com/2012/06/less-is-exponenti...
I rather tend to call what go does regularity and the language design of go does promote said regularity. Regularity in this context means: Go codebases and lots of go code tends to follow very similar patterns.
And this in turn makes it simple to digest even complex code bases, at least to me. A go codebase might just contain more code, but chasing some listener on the result of another listener through multiple annotations and config files in a java codebase... yeh.
And on top of that, this kind of regularity, as well as the kinda small set of libraries to do something makes it easier to get started with the language in my experience, especially for people who aren't seasoned developers.
At the end of the day, I rarely care about the language. I usually just care about my data morphisms. And a good type system helps me immensely in that regard.
You want code to be obvious. The senior engineer smiles when the junior engineer reads his code and says in just seconds "I know exactly what this does and have high confidence that it has no bugs."
Simpler languages facilitate simpler codebases.
I you would design TypeScript from scratch, you might have a point.
It turns out trying to model a dynamic language's typical usage at compile-time is lead to some complicated stuff.
Personally I think it's mostly worth it since most of that is hidden behind API-boundries or (more commonly) in type-definition files and your own code typically either has really simple type annotations or is inferred (i.e. not explicitly type annotated).
But I see your point and it does sorta feel a bit C++-like in this respect sometimes.
It's there primarily so that your development environment can give you accurate feedback when you're using your favourite JS libraries.
The great thing with Typescript is that you don't have to use all its spec to benefit from it, so no user has to know the entire spec.
But you can't complain that Typescript does exactly what it was invented for. Blame Javascript.
At first I loved its simplicity. It did its basic function (getting me from one place to another) quite well. I drove it only occasionally, as I was in college, and it didn't fail me in any way.
Then I graduated and got a job, and started commuting. Once I found myself driving that car every single day, I started to resent its simplicity. In a few occasions it bit me in the ass - one time, my passenger forgot to lock their door after getting out, and overnight someone got in and rummaged through the car and stole a bunch of stuff. That would not have happened if it had automatic locks!
That's how I feel about Go: if you're writing simple microservices occasionally, then it's a good language. But as usage becomes more frequent and serious, the simplicity starts to seriously hold one back. I understand the reasons behind that simplicity, but, as with all things, too much of a good thing can in fact be bad.
I learned how to code "thin" low abstraction C++ and un-clever dynamic code in languages like Ruby and JS as I matured as a programmer, but it takes discipline. When I was younger I fell like so many others into the trap of showing off my command of languages instead of showing off my command of algorithms and ability to solve problems elegantly.
I don't find Go that ugly, especially with standard 'gofmt' formatting. It's so simple the brain learns to parse it as effortlessly as the compiler.
The brilliance of a program is its results / its complexity, with the greatest code ever written being simultaneously the simplest and most impactful.
Go makes that maturity natural and easy. I think it's a great influence on the programming zeitgeist as well as a nice language.
Haven't tried Rust yet but my impression is that its somewhere between C++ and Go. Can anyone comment on Rust's cognitive load?
The reason for this is that you have to learn to work with the compiler and its rules. At first, that’s a struggle. But later, it’s just helpful, and you have to think about things way less because the compiler is helping you keep track of them. It’s like a pair programmer.
Rust and go are not really comparable in this regard, even if rust is much better than most people realize. There are too many ways to approach a problem in rust.
That said, definitely there are plenty of places where rust is a much better language choice than go
I have written some, but not a ton, of go. It is opinionated in ways that are very different than my own opinions, and so I didn’t enjoy it very much. That said, I deeply respect the Go team, and am glad others really like it. I’m not really the best person to compare the two languages, which is why I try not to do it.
For example compare inserting into a list in Python:
list.insert(index, obj)
with apparently idiomatic Go: a = append(a[:i], append([]T{x}, a[i:]...)...)
Or compare Python's `list.reverse()` to Go's recommendation: for i := len(a)/2-1; i >= 0; i-- {
opp := len(a)-1-i
a[i], a[opp] = a[opp], a[i]
}
This seems quite error prone.Idiomatic Go taken from https://github.com/golang/go/wiki/SliceTricks
So in your example, append may need to allocate, and the fact that it returns a new pointer makes that clear.
For the second example, you could use sort.Reverse, but the loop makes the cost of the operation clear.
The real problem is that without generics, such functions actually have to be written in the same fashion as sorting where you provide a function for reading and writing both, as opposed to just working--so the "simple" loops which are indeed error prone actually become less code, and we all use them instead. I highly, highly doubt even the best Google programmer will always get the boundary condition on that reverse a list loop right especially in an emergency or at 3 AM, and this is a very simple case where Go just can't abstract on par with anything-with-generics.
But my point is that you can meet the goal of not hiding operations just with good naming, if that's what you consider a terminal value of good pl design, and don't need to forego abstraction to do it.
Code generators are rarely used and Go has an official built-in dependency management tool: go modules.
Granted this was 4 or 5 years ago so perhaps things have improved.
But if you do it several times, you think "if I just invest time into learning the lathe, I can save a lot more time when making table legs. In fact, I might be able to make a more complicated table, since the time and effort saved when making the legs can now be spent on other things."
If you don't use it, remove it (compiler checks to see if a variable / module import is actually used)
The real pleasure in go vs c/c++ is compiling / linking is easy with the new go modules support, multi-platform out of the box. Kinda like cargo in rust. Kinda.
Downside is.. C/C++ modules are 2nd class citizens that can slow your whole code down.. Conversion is much better, but usually someone has done it yet.. sqlite needs a go port.
I think of go as an advanced assembly language with some c-like idioms and a powerful runtime to handle processor thread scheduling.
I use “generics” in TypeScript but it’s always a very small number of types: usually two, maybe up to 10...
There’s no reason I couldn’t do that without generics, just by writing a couple line plugin for each type that converts it to a common representation.
I’m not convinced that’s even strictly worse than generics from a usability perspective.
I don't know about that. I've tried to build 3 Go applications in the last couple of years and none worked because some of their dependencies had breaking changes in master. Hoping that someone doesn't push a breaking change on master, doesn't change username or doesn't delete their repo doesn't seem very sane to me. At least vendoring is well supported.
I don't agree with other people calling the language simple. It makes doing something like building a web-server from scratch very simple, but the actual language syntax and expressiveness is lacking in my opinion.
Developers must arm themselves with established and tested software design patterns to utilize the language effectively.
The same goes for other languages as well, however Go's emphasis on providing a tiny toolset amplifies the benefits/consequences.
... Also we need generics
It looks positively complex compared to Scheme.
There is no equivalent to this in the Go world. Of course, you could do it, you can do functional programming in any language. It's just not usual. Paradigmatic Go code is simple, imperative style with a lot of mutation. You only need to learn how structures and interfaces work and your good to go.
In practice, you'd just use a library function for pretty much all of those.
Nobody writes those from scratch in Lisp or Scheme except as an exercise.
Also, once you do get the hang of it, recursion is a much simpler and more natural way of expressing many algorithms. But it too is not necessary in Lisp or Scheme, and you're welcome to write the iterative equivalent, if that's what floats your boat.
As for writing in an imperative vs functional style, I personally find the latter simpler and more reliable. But nobody forces you to write in a functional style in Lisp or Scheme. They're both multi-paradigm languages, and you can write in an imperative style in either, if you prefer.
Go's Simplicity is its weakness for XXX thing.
I find these constant take one aspect of a language and paint the world with it to be unhelpful and distracting. However based on the comment section HN eats it up, no wonder people keep writing them. Its like Instagram for nerds I guess.
I can not create and control native threads. While Goroutines are nice and needed feature some of my software simply can not function without the native threads. Hence Go is no go for me.
Go was created so Google people could write server-side stuff in something less brittle than C++. It's a good match for that. The library stability is a good thing. Those are libraries Google uses internally, so you know they're executing millions of transactions a second somewhere. The error cases have been encountered already.
That sucks! Why sh I kid I help the company solve their business problem when I could be spending all afternoon debugging (insert language issue)
There is a balance to reach, and languages like Java do a much better job at this than golang.
People complain about the lack of generics but I wonder if they didn't allow it to avoid an abstraction explosion, at the cost of redudant/copypasting.
I wonder if Go ends up sharing Perl's fate. Two languages that make people feel very productive, but don't seem to scale very well.
The "simplicity" is shown in the amount of code generator frameworks.
Object Pascal is already a PhD level language according to Go's design point of view.
While I loved my experience as Oberon user/dev, and it opened my mind to systems programming using GC languages, my favourite languages on Wirth's linage aren't done by him, rather influenced by his creations.
Modula-3 (which comes from Mesa/Cedar, not Oberon), Active Oberon (another ETHZ team), Zonnon, Delphi.
go run main.go
---------
OP edited in "garbage collector" over "interpreted".
More seriously, as of last year at least mark phase pauses could be significant, on the order of seconds for large heaps.