[HN Gopher] Fixing for loops in Go 1.22
___________________________________________________________________
Fixing for loops in Go 1.22
Author : todsacerdoti
Score : 562 points
Date : 2023-09-19 19:34 UTC (1 days ago)
(HTM) web link (go.dev)
(TXT) w3m dump (go.dev)
| assbuttbuttass wrote:
| for _, informer := range c.informerMap { informer :=
| informer go informer.Run(stopCh) }
| for _, a := range alarms { a := a go
| a.Monitor(b) }
|
| Not sure what the difference could be, but let me take a guess.
| In one case, the loop variable is a pointer, and in the other
| case a value. The method call uses a pointer receiver, so in the
| value case the compiler automatically inserts a reference to the
| receiver?
| thebears5454 wrote:
| It's definitely something like that where the compiler knows to
| grab the value.
| pdimitar wrote:
| Since iterating on maps in Go always results in copying the
| value then I'd guess the first piece of code does what is
| expected due to that, and the second does not because `a` will
| only ever have the value of the last element of `alarms` (the
| original problem described in the article).
| Cthulhu_ wrote:
| I'm looking at the naming, the top one is a map, the bottom one
| is a slice; that's where my internal knowledge ends though. I
| know a slice will have a backing array on the heap so there's
| some pointers / references involved.
| assbuttbuttass wrote:
| I managed to track down the original code containing these
| snippets using the GitHub code search tool:
|
| https://github.com/adobe/kratos/blob/93246f92d53feba73743dbf...
|
| https://github.com/StalkR/goircbot/blob/6081ed5d1d74f01767d7...
|
| The difference is that in one case, informer is an interface,
| so the method call resolves informer.Run immediately and
| there's no issue. In the other case, a is a struct Alarm, and
| gets copied by value, and the Monitor method takes a pointer
| receiver. So my original intuition was right, the compiler is
| essentially translating go a.Monitor(b)
|
| into go (&a).Monitor(b)
|
| Which has a reference to the loop variable, and creates an
| issue.
| Spiwux wrote:
| I have such a love-hate relationship with this language. I use it
| professionally every single day, and every single day there are
| moments when I think to myself "this could be solved much more
| elegantly in language X" or "I wish Go had this feature."
|
| Then again I also can't deny that the lack of ""advanced""
| features forces you to keep your code simple, which makes reading
| easier. So while I hate writing Go, I like reading unfamiliar Go
| code due to a distinct lack of magic. Go code always clearly
| spells out what it does.
| bvinc wrote:
| "Love-hate relationship" were the exact words that I used when
| I used go professionally every day.
|
| I could complain all day about things the language does
| obviously wrong, often in the name of simplicity. But after all
| my complaints I still admit it's a very good choice for certain
| kinds of software and software companies.
| christophilus wrote:
| I'm in the same boat. Every once in a while, I go back and look
| at my old Haskell, OCaml, and Go code, and I remember why I
| like Go. Generally, I can hop back into my old code easily.
| That's not true with more advanced languages. I just can't
| resist the urge to be clever when writing them. OCaml is still
| pretty nice, though. Not gonna lie.
| [deleted]
| stevepotter wrote:
| Long time software engineer, just coming off 4 years of Kotlin
| into Go. Love-hate describes it for me. It's just not as much
| fun and feels sterile. I get the whole "just write the damn
| code" argument, but unfortunately for me I get fulfillment out
| of writing code, and Go isn't doing it for me. I've been around
| for a long time and experienced all the various language
| philosophies. The Go dogma is especially frustrating.
| Everything I say is met some automatic recycled response. No
| thanks
| Cthulhu_ wrote:
| Another commenter described it quite succinctly; to paraphrase,
| Go isn't made for you, it's for all the other developers that
| will have to work with your code - including future you.
| JyB wrote:
| The first point cannot bother you after you've correctly
| realized your second point. The more empathy you have for your
| future-self or your peers, the clearer it becomes.
| jamespwilliams wrote:
| Go is the worst language, except all the others
| noelwelsh wrote:
| This was also an issue in Javascript (e.g.
| https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...)
|
| It's somewhat amusing to see Go rediscover old ideas in
| programming language theory, given the stance against PLT that
| the Go developers took in the early years of the language.
| stouset wrote:
| The entire story of go seems to be learning through repeating
| the same mistakes as other languages, one at a time.
|
| Nil being another big one. Even more impressively they doubled
| down on this mistake with _typed_ nils. Even if you explicitly
| do a comparison with nil you can still shoot yourself in the
| foot because it was a different nil than the one you compared
| against.
| TwentyPosts wrote:
| Wait what? Do you have an example or more information on
| this?
| Thaxll wrote:
| op is confused with nil interface.
| gwd wrote:
| Basic example: func foo() *bar {
| // ... if something_wrong { return
| nil; } } var x
| interface{} x = bar() if
| x != nil { // Dereference x }
|
| This will crash if `foo()` returns nil, because it's
| checking if `x == interface{}(nil)`, which is false. What
| you wanted to check was whether `x == *bar{nil}` or one of
| the other nil types that implements the interface; which
| must be done with `reflect.ValueOf(x).IsNil()`.
|
| https://github.com/golang/go/issues/30865
| kiitos wrote:
| This is a great example of code that compiles, but would
| never pass code review at any decent organization.
| Specifically, you'd never assign a concrete return value
| like *bar to an interface{} and expect `x != nil` to
| behave like this code would imply.
| stouset wrote:
| Yes, it's a contrived example. But it's not like this is
| some obscure thing that no go programmer has ever run
| into in practice. It's something I'd wager almost
| everyone has encountered if they've used it longer than a
| year.
|
| https://dave.cheney.net/2017/08/09/typed-nils-in-go-2
|
| If Dave Cheney says it hits every go programmer at least
| once, it caused hours of consternation for his coworkers,
| and it even has its own entry in the language FAQ, I
| don't know what else to tell you.
| kiitos wrote:
| Yes, it's a not-uncommon gotcha or foot-gun. No argument
| there. But, like many other gotchas and foot-guns, they
| are not too difficult to spot in code review.
| gwd wrote:
| ...if you have experienced golang developers who have
| scars on their feet. If you have someone who's an
| experienced developer but only used golang for a few
| months, they might not catch it, which means a hard-to-
| find bug that got into your code.
|
| Furthermore, even for experienced developers, there's a
| limit to how much context / rules / whatever your brain
| can keep. This footgun takes up space and intellectual
| energy that could be used for something else.
|
| All things being equal, a language that doesn't have this
| kind of footgun is better than one that does: less
| experienced reviewers will let fewer bugs slip through,
| and more experienced reviewers will either spend less
| effort reviewing (meaning the mental energy can be used
| somewhere else) or will have more review capacity
| (meaning they'll find more bugs / improve the code more).
| gwd wrote:
| This is the actual code that caused me to write the
| ticket above (be warned, I wouldn't consider it amazing
| code; my first foray into writing a web app as a side
| project, just trying to get something that works):
|
| https://github.com/gwd/session-
| scheduler/blob/master/handle_...
|
| Basically, I have several pages I'm rendering, which have
| common prerequisites regarding checks, and common
| handling processes (passing some sanitized data to a
| template). The *GetDisplay() functions take a structure
| from the "database" layer and sanitize it / process it
| for handing to the templates. The two *GetDisplay()
| functions return pointers to two different types,
| appropriate for the template to which they will be
| passed; and return nil if there's an issue.
|
| So I have a map, `data` of type `map[string]interface{}`
| that I pass into the templates; and two different paths
| set `data["Display"]`; then at the end I want to check if
| either of the `*GetDisplay()` functions returned `nil`.
| So naturally, the first version of the code checked
| `data["Display"] == nil`, which was always false, since
| it was implicitly checking `data["Display"] ==
| interface{}(nil)`, but the value in case of an error
| would be either `*DiscussionDisplay(nil)` or
| `*UserDisplay(nil)`.
|
| I mean, sure, there are other ways to structure this; I
| could return an error or a boolean in addition to
| returning nil. But 1) the only reason to do that is to
| work around this language limitation 2) it's a "foot gun"
| that it's easy to fall into.
|
| And sure, a golang developer who'd shot themselves in the
| foot a few times with this would catch it during review;
| but I don't think a bunch of newer developers would catch
| it, even if they had extensive experience in other
| languages.
| kiitos wrote:
| data := map[string]interface{}{}
|
| So this is the problem, basically. Go isn't a dynamically
| typed language, and doesn't really let you create an
| arbitrary map of keys to objects like e.g. Javascript or
| Python does. Any time you see `map[something]interface{}`
| that's a huge red flag that something is fucky. In your
| case you want to define `data` as a struct type with a
| Display field (and whatever else). if
| ... reflect.ValueOf(display).IsNil() {
|
| Any use of `package reflect` in application code is a
| similarly huge red flag. 99 times out of 100 it's a
| design error that ought to be fixed.
| Spiwux wrote:
| e.g. var typeA Interface = (*TypeA)(nil)
| println(typeA == nil) // false
| println(typeA == (*TypeA)(nil)) // true
|
| Yes really https://go.dev/play/p/sz44kJW8OuT
| Cthulhu_ wrote:
| And yet, they started off with a language that fixes the
| mistakes of the main language it tried to replace - c / c++.
| Mistakes like pointer arithmetic and manual memory
| management, not zeroing out memory before use, compile times,
| the list goes on.
|
| And generics is an example of a language feature they took
| their time for, to avoid making the same mistakes as e.g.
| Java did, where generics ended up taking up half the language
| spec and compiler / runtime implementation.
| akira2501 wrote:
| Programming languages are an exercise in compromise, not pure
| application of theory. Well, except maybe Haskell and it's ilk,
| but this should be your expectation of most languages and
| generally not a surprise.
| noelwelsh wrote:
| There's no need for compromise in this case. This issue is
| something I learned about in the late 90s / early 2000s when
| I started reading about programming language theory. It's
| found in introductory textbooks.
| FridgeSeal wrote:
| I think OP's point is that given the go devs casual disregard
| for every development in PL theory and design over the last
| 30 years, it's amusing to watch them rediscover half the
| issues from scratch.
| skybrian wrote:
| This meme is commonly repeated on HN, but I think it's
| inaccurate or at least greatly exaggerated.
|
| There's nothing _casual_ about Go 's approach to language
| design, and I haven't seen any evidence that they're
| _unaware_ of what other languages do.
|
| I also haven't seen much criticism of languages other than
| C++ or Java.
| FridgeSeal wrote:
| Sum/Product types. Generics/type-parameters. Bizarre
| handling of nil in places. Error handling that's like
| some deliberately crippled version of a Result<T,E>. The
| absolutely unhinged decision about zero-value-defaults
| for types. I'm sure other people can think of some more,
| but that's the ones I can think of off the top of my
| head.
|
| Non PL theory but related: incorrect implementation of
| monotonic clocks, and then refusing to fix it because
| "just use google smear time bro".
| skybrian wrote:
| Everyone who disagrees with you is "unhinged." This is
| just name-calling.
| FridgeSeal wrote:
| But they're like, genuinely wild decisions? Unhinged is a
| great description!
|
| The error design can return the value, or an error, or
| both, for some inexplicable reason and you can check it -
| or not - and if your function returned some indication of
| severe error, and you happen to not check it, you can
| totes just continue your program, in who knows what
| invalid state. Oh and also, because the devs are
| apparently deathly allergic to abstractions of apparently
| kind, you've got to do this janky little if err != nil
| check at. every. single. point. Which occludes your
| fundamentally important logic in pointless line noise,
| with zero opportunity for streamlining or improving the
| logic or guarantees.
| kiitos wrote:
| Go definitely implements monotonic clocks correctly?
|
| https://pkg.go.dev/time#hdr-Monotonic_Clocks
| FridgeSeal wrote:
| It definitely didn't used to lol:
|
| https://fasterthanli.me/articles/i-want-off-mr-golangs-
| wild-...
| kiitos wrote:
| That article is so cringeworthy
| FridgeSeal wrote:
| I disagree, but the style isn't for everyone.
|
| However, that still doesn't mean that it's _wrong_.
| bobbylarrybobby wrote:
| If they're aware of how other languages either handle
| these issues or suffer the consequences of not handling
| them, then it sure is odd that they consciously decided
| to introduce the same issues into their own language,
| only to fix them down the road.
| skybrian wrote:
| Given Go's success, it seems like fixing certain footguns
| _much later_ actually worked out pretty well for them?
| That doesn't necessarily mean it was right, but it was
| perhaps not as big a deal as some people assume.
| doctor_eval wrote:
| It's also amusing to see someone talk about JavaScript and PLT
| in the same sentence, given the practical origins of JS.
| noelwelsh wrote:
| It's not making any claims about the quality of design in JS
| here. I'm literally not talking about them in the same
| sentence for this reason. I'm, instead, merely noting that it
| had the same issue. Looks like it was fixed in 2014:
| https://nullprogram.com/blog/2014/06/06/ C# also had the same
| issue, as noted elsewhere in the comments.
| [deleted]
| doctor_eval wrote:
| It simply seems disingenuous to talk about how this was an
| issue in JS and then go on to say that Go devs have a
| "stance against PLT". JS is not renowned for being an
| exemplar of PLT, so why would the Go developers use it as a
| reference point for their own design? JavaScript can't even
| take the address of a variable - which is the underlying
| problem here.
|
| In any case - I seem to remember that they discussed the
| rationale for the original decision in the release notes
| for go 1.21, along with additional context.
|
| For whatever it's worth, I don't see any evidence that Go
| is specifically antagonistic to programming language theory
| at all - the existence of first-class constructs like
| channels and closures suggests otherwise. There are always
| costs and tradeoffs involved in adopting certain
| theoretical paradigms, and PLT is subject to fashion as
| much as any other endeavour.
|
| Go focusses on simplicity, and when talking about
| simplicity I really like this quote from Dijkstra:
| "Simplicity requires hard work to be obtained and education
| for its appreciation, and complexity sells much better."
| [0]
|
| I think Go works hard to be simple, and sometimes that
| comes across as being simplistic. Indeed, I was sceptical
| of Go when I set out to learn it, but having spent enough
| time with it to consider myself a professional Go
| developer, I also find that enjoy coding more than I have
| for many years, `err != nil` notwithstanding.
|
| [0] https://www.cs.utexas.edu/users/EWD/transcriptions/EWD1
| 0xx/E...
| [deleted]
| anderskaseorg wrote:
| MDN goes into great detail, but the important point is that
| JavaScript fixed this with let and const. > a
| = []; for (i = 0; i < 3; i++) a.push(() => i); a.map(f => f())
| [ 3, 3, 3 ] > a = []; for (let i = 0; i < 3; i++)
| a.push(() => i); a.map(f => f()) [ 0, 1, 2 ] >
| a = []; for (i of "abc") a.push(() => i); a.map(f => f())
| [ 'c', 'c', 'c' ] > a = []; for (const i of "abc")
| a.push(() => i); a.map(f => f()) [ 'a', 'b', 'c' ]
| zeroimpl wrote:
| In contrast, Java would have a compile error so long as the
| variable was not declared final.
| ben0x539 wrote:
| But isn't it _completely wild_ that the `for (let i =`...
| version works like that? What does the loop 'desugar' into
| if written as a while-loop?
| anderskaseorg wrote:
| Before let/const, scopes could only be introduced at
| function level, so @babel/plugin-transform-block-scoping
| transpiles it using an extra function:
| var _loop = function (i) { a.push(() => i);
| }; for (var i = 0; i < 3; i++) {
| _loop(i); }
|
| The key is that the scoping happens for each iteration, not
| around the entire loop. That detail is nonobvious, given
| how many other languages have gotten it wrong, but I
| wouldn't say it's wild.
|
| (If you're curious how Babel deals with the more
| complicated cases of break/continue, labelled
| break/continue, and return, try it out at
| https://babeljs.io/repl.)
| ben0x539 wrote:
| Right, the wild thing for me is when you mutate `i` in
| the loop body. So at the same time `i` is scoped to the
| iteration so that you can capture it, but also mutating
| it affects the loop-scoped `i` that is incremented
| between iterations and checked for the termination
| condition. The iteration-scoped `i` is assigned back to
| the loop-scoped `i` at the end of the loop body. So if
| you have a closure close over `i` in the loop body and
| mutate it, whether that mutation affects the actual loop
| variable depends on whether the closure is called during
| the iteration it was created in or during a later
| iteration. Kinda spooky, but sure, less of a footgun than
| the original behavior.
| biomcgary wrote:
| Is this loop variable problem really a theory issue (if so,
| does it have a label)? Or, primarily a practical one? Is there
| a database of known and historical programming language
| problems that are nicely tagged with "theory problem",
| "frequent foot-gun", etc?
|
| Could an LLM coupled with a constraint solver create the
| perfect language (for particular domains, e.g., performance)?
| Or, just use Rust ;-)?
| noelwelsh wrote:
| In contrast to the view of many, programming language theory
| is very closely intertwined with programming practice. It
| both drives practice and reacts to practice.
|
| In this particular case I imagine the issue was uncovered
| some time in the 1970s, which is when lexical scoping came to
| the fore in Scheme (in the US) and ML (in Europe). It's a
| fairly natural problem to run into if you're either thinking
| deeply about the interaction between closures (which capture
| environments) and loop constructs, or if you're programming
| in a language that supports these features.
| philosopher1234 wrote:
| What stance against PLT are you referring to?
| pseudonom- wrote:
| Probably quotes like:
|
| "It must be familiar, roughly C-like. Programmers working at
| Google are early in their careers and are most familiar with
| procedural languages, particularly from the C family. The
| need to get programmers productive quickly in a new language
| means that the language cannot be too radical."
|
| And not including sum types despite having a sum-type-shaped
| hole in the language (`if err != nil`).
|
| And some of the discussion about "why no generics" seemed
| kind of divorced from existing PL knowledge on the topic.
| tensor wrote:
| These were all intentional tradeoffs though, not any
| ignorance of theory. Also, it's pretty rich for someone to
| be complaining about Go while referencing Javascript of all
| languages. Javascript's design flaws are legendary. And I
| mean no disrespect to the creators of Javascript, they had
| to deal with some crazy last minute change requests to the
| language.
| kaba0 wrote:
| With all the crazy warts that JS has, it is at least a
| lisp-like very dynamic language if you squint (a lot) at
| it. Its greatest fault is probably leaving out integers
| (I can't even fathom why they decided on that, floats
| can't represent properly ints).
|
| Go is just simply badly designed, relying on hard-coded
| functionality a lot.
| Cthulhu_ wrote:
| Which is better, hard-coded functionality or magic? I
| don't believe it's badly designed, since every design
| decision is deliberate; bad design would be accidental.
| TwentyPosts wrote:
| Ehhh, I see absolutely no evidence that the Go developers
| were particularly _aware_ of theory. It really feels more
| like they just were used to thinking in terms of C, and
| built a language which is kind of like C.
|
| Go also has some really weird stuff in it, such as named
| return values.
|
| Frankly, the lack of sum types hurts the most. The
| language would just be a lot better with a unifying
| Result type in the library. And don't give me any of that
| "oh, they tried to keep the language simple!" stuff.
|
| Intuitively, sum types are laughably simple. Everyone
| understands "It's one of these possible values, so you
| need to check which one it is and then handle that
| situation." They are more simple than enums on a
| conceptual level! Sum types are just not how
| C-programmers think about the world.
| ianlancetaylor wrote:
| As it happens, we considered sum types quite seriously in
| the early days. In the end we decided that they were too
| similar to interface types, and that it would not help
| the language to have two concepts that were very similar
| but not quite the same.
| https://groups.google.com/g/golang-
| nuts/c/-94Fmnz9L6k/m/4BUx...
|
| There is a lot of discussion of sum types in Go at
| https://go.dev/issue/19412.
| TwentyPosts wrote:
| When I did research on this topic ages ago I read both of
| these links, and I'm very familiar with the arguments.
|
| I also vehemently disagree with them, and I think that
| the way code is factually written in practice is on my
| side: People who propose sum types commonly refer to the
| Option<T> or Result<S,E> types in Rust. These are types
| which are _almost exclusively_ used as _return types_.
|
| Interface types are the opposite. They're used as _input
| types_ , and almost never to distinguish between a
| concrete, finite list of alternatives. They are used to
| describe a contract, to ensure that an input type
| fulfills a minimal set of requirements, while keeping the
| API open enough that other people can define their own
| instances.
|
| The fact that Go in fact does _not_ use interface types
| for its error handling is a pretty good argument in favor
| of that, I 'd say.
|
| The thing is, at this point it doesn't matter, sadly.
| Adding sum-types to the language now would be
| unsatisfying. You would really need proper integration as
| the primary tool for error handling in the standard
| library (and the ecosystem), and that's unlikely to
| happen, even less likely than a Go 2.0.
|
| EDIT: Just to make it clear, I think not wanting to add
| sum types to the language is understandable at this
| point. The real shame is that they were not in the
| language from the beginning.
| ianlancetaylor wrote:
| Go made a deliberate decision to use multiple results
| rather than Option<T> or Result<S, E>. It's of course
| entirely reasonable to disagree with that decision, but
| it's not the case that the Go language designers were
| unaware of programming language theory or unaware of sum
| types. Although you suggest that you want sum types as
| the primary tool for error handling, Go made a different
| decision, not due to accident or lack of knowledge, but
| intentionally.
|
| (Separately, I'm not entirely sure what you mean when you
| say that Go doesn't use interface types for its error
| handling, since the language-defined error type is in
| fact an interface type.)
| TwentyPosts wrote:
| Fwiw, I didn't mean to imply that they don't _know_ any
| language theory, just that the language doesn 't seem to
| reflect it. I don't think this itself should be a
| controversial statement, by the way, Go aims to be a
| simple language, and the last thing it needs is monads.
|
| Frankly, I'm just the type of person who doesn't
| understand why it is possible to silently drop error
| values in Go (even accidentally), see f,
| err := os.Open("foo.txt") g, err :=
| os.Open("bar.txt") if err != nil {
| return }
|
| while the language is simultaneously very strict about
| eg. unused imports.
|
| It seems like a pretty severe flaw for a language that
| takes pride in its explicit error handling, and a deep
| dive into why this flaw was acceptable to the creators
| would be really interesting.
|
| For now though, instead of sum types we ended up with
| features such as named returns (???). I imagine some of
| the complexity here was about not wanting to introduce an
| explicit tuple-type, since a Result<S, E>-type doesn't
| compose with multiple returns. (I feel like there should
| be some workaround here. Maybe anonymous structs + some
| sort of struct/tuple unpacking, but I could see it
| getting gnarly.)
|
| > I'm not entirely sure what you mean when you say that
| Go doesn't use interface types for its error handling,
| since the language-defined error type is in fact an
| interface type.
|
| What I meant is that this specific usecase of sum-types
| (ie. error-unwrapping) is not something that interfaces
| in Go are used for. Error-handling in Go is done via
| multiple return values. This goes against the common
| claim that "sum types and interfaces are too similar/have
| the same uses", and should count for something,
| considering that explicit error handling is a big
| component of Go.
| kiitos wrote:
| > I see absolutely no evidence that the Go developers
| were particularly aware of theory
|
| Rob Pike and Ken Thompson are theory-unaware. Sure. Yes.
| This position is good and defensible.
| TwentyPosts wrote:
| The claim wasn't that they _are_ unaware, the claim was
| that it doesn 't seem to show in the design of Go.
|
| I think Go is a perfectly fine language, and I respect
| the goal to stay clear of complexity, but when looking at
| any particular Go feature, then it's easier to explain
| the decision with "They are used to thinking in terms of
| C-idioms." than with any particular brilliance or
| awareness of PL-theory.
| hollerith wrote:
| I believe it: Thompson is old enough and Pike is arrogant
| enough not to have learned the relevant programming-
| language theory.
| kiitos wrote:
| Nice joke.
| [deleted]
| hollerith wrote:
| Thompson turns 80 this year. In what years does Thompson
| become old enough that you start to entertain the
| possibility that not everything he says or does is the
| result of having learned and understood all the possibly-
| relevant work, including the recent work?
|
| I misspent a few thousand hours of my life on the 9fans
| mailing list long ago when Pike was very active on it,
| and my non-joking assessment is that ever since he
| finished his PhD or shortly after, Pike has probably felt
| he knows all he needs to learn about programming-language
| design except for the things he and the people in his
| immediate social environment invent.
|
| Bell Labs was never good at designing programming
| languages. Did you know that in the Bourne shell (and
| possibly in all the other shells) you can have a
| statement of the form $foo = bar which will assign bar to
| the variable whose name is the value of foo? (Emacs Lisp,
| an old language, has the same functionality in the form
| of a function named "set", but most Emacs Lisp
| programmers know to avoid it.) Well, I found that
| statement in a shell script written by one the Bell Labs
| guys, and the shell script was not doing anything fancy
| like interpreting a programming language or defining a
| new PL feature (not that it is sane to do either of
| things in a Unix shell).
|
| None of what I say is more than a wisp of a reason not to
| choose Golang IMHO.
| kiitos wrote:
| You're moving the goal posts. The claim isn't that Ken
| Thompson or whoever is flawless. The claim is that he
| isn't stupid.
| xyzzy_plugh wrote:
| Have you looked for any evidence? There is plenty.
|
| Sum types have been discussed since before the initial
| open source release of the language, at least according
| to some of the issue threads such as
| https://github.com/golang/go/issues/19412
|
| If they're laughably simple, then please contribute a
| proposal for how to add them to the language. You'll find
| no one is really fighting against the concept of sum
| types.
| TwentyPosts wrote:
| They're 'laughably simple' on a conceptual level. Adding
| them to the language in retrospect might genuinely be a
| bad idea at this point.
|
| The thing which I am personally salty about is that they
| weren't there to begin with, and that the language wasn't
| built with them in mind.
| xyzzy_plugh wrote:
| Go development began circa 2007, I do not believe sum
| types were common place nor do I believe the average
| developer was aware of them at the time. I'm not saying
| you're wrong, but given even Scala didn't add support
| until 2010 they do not seem like something sorely obvious
| that should have been there from the beginning. In
| hindsight, sure.
| scottlamb wrote:
| I've written a tiny bit of Go and am aware of the general problem
| this solves. I don't get their more subtle examples (the
| letsencrypt one or "range c.informerMap" vs "range alarms".
|
| When you do "for k, v := range someMap", is "v" of the map's
| value type (and one binding for the whole loop, copied before
| each iteration)? This would explain the problem, but I would have
| expected "v" to be a reference into the map, and I couldn't find
| the answer in a quick skim of the "For statements with range
| clause" in the spec. I'm probably looking in the wrong place
| because I touch Go rarely...
|
| [1] https://go.dev/ref/spec#For_statements
|
| edit: oh, the answer is in the "code block"-formatted table.
| Guess I had banner blindness. "v" is the copied value, not a
| reference. I'm surprised!
| assbuttbuttass wrote:
| I managed to track down these code snippets. Feel free to check
| out these links if you're curious:
|
| https://github.com/adobe/kratos/blob/93246f92d53feba73743dbf...
|
| https://github.com/StalkR/goircbot/blob/6081ed5d1d74f01767d7...
|
| Basically the compiler is translating go
| a.Monitor(b)
|
| into (&a).Monitor(b)
|
| due to automatic dereferencing
| tedunangst wrote:
| If you have a map of string to int, then v is of type int. It's
| a value. It's not pointer to int.
| scottlamb wrote:
| Is the expectation that you simply won't create a
| `map[...]expensivetocopyvalue`, but instead always do
| `map[...]*expensivetocopyvalue`?
| tedunangst wrote:
| The expectation is the compiler does what you tell it? If
| you want pointers in your map, you can do that too.
| Arnavion wrote:
| They're asking that, if the programmer wants the map to
| store expensivetocopyvalue semantically but also doesn't
| want to have iteration generate expensive copies, does
| the programmer have to change the map to store
| *expensivetocopyvalue instead?
|
| Anyway I believe the answer is that expensivetocopyvalue
| is not a type that exists in golang, because golang's
| "copy" operation is always a simple bitwise copy ala C
| struct copy / Rust's Copy trait, not like C++ copy ctor /
| Rust's Clone trait that can be arbitrarily expensive.
| morelisp wrote:
| In Go, `expensivetocopyvalue` can still be achieved via
| an enormous (e.g. multi-KB/MB) structure (which is most
| literally expensive to copy) or something containing a
| lot of pointers (which is not really expensive to copy
| but will start to pressure the GC).
| [deleted]
| [deleted]
| konart wrote:
| No expectations whatsoever. You can use pointer or a
| values. It's up to you.
| scottlamb wrote:
| That's silly. Language constructs and APIs are always
| made with expectations for how they're used, stated or
| not. You can write code that compiles without
| understanding and matching those expectations but it
| probably won't be good code.
|
| I'm asking because I think if it were expected that folks
| used large/expensive-to-copy map values, this construct
| would return a reference instead of copying. In Rust for
| example, the std library's "normal" [1] iterators return
| references.
|
| [1] not those returned by into_* or drain.
| konart wrote:
| Here an example: https://go.dev/play/p/He0lBEYZJ03
|
| Value is always a copy. Either a copy of a struct (in
| case of map[T]struct{}) or a copy of a pointer (in case
| of map[T]*struct{})
| morelisp wrote:
| Returning references to storage within the map would be a
| substantial footgun without borrowing.
| scottlamb wrote:
| Thanks, useful reply. I just realized that myself:
| https://news.ycombinator.com/item?id=37576558
|
| The peer comments along the lines of "the expecation is
| it does what it does" are not so helpful from a
| perspective of learning to write code that is in harmony
| with the language philosophy.
| [deleted]
| erik_seaberg wrote:
| Go doesn't support pointers to map keys or values. It does
| support pointers to array slots, but for-range copies each slot
| rather than giving you a pointer to it.
| scottlamb wrote:
| I suppose that makes sense when I think about it for a bit.
| My recent expectations come from work in Rust. There the
| language prevents you from mutating a map while holding a
| reference into it. Go doesn't have a mechanism to prevent
| that (except the one you said, simply not supporting those
| references at all). If you had a reference into a map that
| was resized because of a subsequent mutation, your reference
| would have to keep the whole previous map alive and point to
| different memory than a reference acquired since then. Both
| seem undesirable.
|
| With array slots, the same issue is present but is a bit more
| explicit because those resizes happen with `mySlice =
| append(mySlice, ...)`.
| erik_seaberg wrote:
| I think the slice append semantics are very error-prone,
| and it would have been better if a slice was a shareable
| reference to a single mutable thing, like a map (or a list
| from Python or Java or ...)
| konart wrote:
| >If you had a reference into a map
|
| Maps in golang are of reference type, just to be clear.
|
| https://go.dev/blog/maps
|
| implementation: https://go.dev/src/runtime/map.go
| parhamn wrote:
| > as a consequence of our forward compatibility work, Go 1.21
| will not attempt to compile code that declares go 1.22 or later.
| We included a special case with the same effect in the point
| releases Go 1.20.8 and Go 1.19.13, so when Go 1.22 is released,
| code written depending on the new semantics will never be
| compiled with the old semantics, unless people are using very
| old, unsupported Go versions
|
| How does this work? If I pull in a package that decided to pin
| 1.22 (as they should) and I compile with 1.18, would it compile
| or error that I need to use the 1.22 compiler?
| icholy wrote:
| They did something sneaky. In go 1.21, they changed the version
| number format in the `go.mod` files. So trying to build with go
| 1.18 will result in: go: errors parsing go.mod:
| go.mod:3: invalid go version '1.21.0': must match format 1.23
|
| However, this will only happen if you use the `go mod init` to
| create your module. If you manually specify `go 1.21` in your
| `go.mod`, it will build without complaining.
| yankput wrote:
| They way I understand this, with go 1.18, 1.22 module as a
| dependency will compile and produce errorneous logic (!!) if it
| depends on this feature
|
| Thus it will be actively dangerous using go 1.18. I understand
| it like that.
|
| With go 1.19, you will get a compiler error.
|
| But as go is not fixing security bugs in old releases and std
| library, I think it is dangerous to use them anyway.
| jerf wrote:
| "But as go is not fixing security bugs in old releases and
| std library, I think it is dangerous to use them anyway."
|
| A bit of a harsh way to phrase that. In my experience, the
| backwards compatibility promises have been very good, and the
| way you stay up-to-date with security fixes and bugs in the
| standard library is to upgrade Go.
|
| I know that may strike terror in the hearts of developers
| used to the nightmare that major version upgrades can be in
| other languages, where a major version upgrade gets a multi-
| week task added into the task tracker, but it's completely
| routine for me to upgrade across Go major versions just to
| get some particular fix or to play with a new feature. I
| expect it to be a roughly five minute task, routinely.
|
| The only thing that has bitten me about it is arguably not
| even Go's fault, which is its continuing advances in TLS
| security and the increasing fussiness with which it treats
| things connecting with old-style certificates. I can't even
| necessarily disagree... I would also like to upgrade them but
| while it's my server, the clients connecting to it are using
| certs that are not mine and it's out of my control.
| yankput wrote:
| > A bit of a harsh way to phrase that. In my experience,
| the backwards compatibility promises have been very good,
| and the way you stay up-to-date with security fixes and
| bugs in the standard library is to upgrade Go.
|
| I don't think we disagree? There is no reason to use old
| version of go.
|
| I speak about grandparent comment who wanted to still run
| go1.18. It is not a good idea to still run go1.18, as it
| doesn't get security updates.
| packetlost wrote:
| Yes.
| jchw wrote:
| Interestingly, though, if you use Go 1.21 and a module declares
| a later version of Go, the default behavior is actually to go
| fetch a newer toolchain and use it instead[1]. It's a pretty
| cool feature, but I am a bit on the fence due to the fact that
| it is surprising and phones home to Google-controlled servers
| to fetch the binaries. That and the module proxy are for sure
| two of the most conflicting features in Go and I'd feel a lot
| better about them if Go was controlled by a foundation that
| Google merely had a stake in. Alas.
|
| edit: Actually, though, I just realized what I am talking about
| is different than what you and your quote is talking about,
| which is what happens when you have a dependency that declares
| a different version, not the current module. Oops.
|
| [1]: https://go.dev/blog/toolchain
| tedunangst wrote:
| You should get a compile error.
|
| But your code, when compiled with go 1.22, will still have go
| 1.18 semantics.
| krackers wrote:
| I think https://eli.thegreenplace.net/2019/go-internals-
| capturing-lo... describes the problem in more detail?
| davidw wrote:
| That's a much better explanation for someone like me who isn't
| very familiar with Go.
| msteffen wrote:
| This post was great--thanks for posting it!
|
| Interestingly, the reason the old i := i trick works is not at
| all what I thought!
|
| The trick, for reference: for i := 0; i < 5;
| i++ { i := i // the trick go func() {
| print(i) }() }
|
| What I assumed happened:
|
| - The escape analyzer sees that the new `i` is passed to a
| goroutine, so it is marked as escaping its lexical scope
|
| - Because it's escaping its lexical scope, the allocator
| allocates it on the heap
|
| - One heap allocation is done per loop iteration, and even
| though the new `i` is captured by reference, each goro holds a
| unique reference to a unique memory location
|
| What actually happens:
|
| - Go's compiler has heuristics to decide whether to capture by
| reference or by value. One component of the heuristic is that
| values that aren't updated after initialization are captured by
| value
|
| - The new i is scoped to the for loop body, and is not updated
| by the for loop itself. Therefore it's identified as a value
| that isn't updated after initialization
|
| - As a result, the Go compiler generates code that captures `i`
| by value instead of by reference. No heap allocations or
| anything like that are done.
|
| I recognize that the latter behavior is better, but if anyone
| with intimate knowledge of Go knows why the former doesn't
| (also) happen (is that even how Go works?) I would love to find
| out!
| xiaq wrote:
| I don't think it's so complex. Without i := i, there's only
| one i. With i := i, there's one i per iteration.
|
| Closure captures are always by reference.
|
| Heap vs stack allocation don't affect the language semantics.
| kevincox wrote:
| Yup. The linked article is a little confused. It thinks
| that an optimization to pass by value is affecting the
| behavior. In reality it only passes by value when it is
| indistinguishable from passing by reference (and it thinks
| it would be cheaper).
| continuitylimit wrote:
| There is no "trick". It's the language spec! That go func can
| take arguments. Just add the argument for clarity. The
| "trick" here is saving the declaration in the go func's
| signature. 'go func(i0 int) { .... }(i)'
| [deleted]
| MatthiasPortzel wrote:
| JavaScript had a very similar problem. If the loop variable is
| declared with the old `var` then it will not capture the
| variable. "New-style" variables declared with `let` are scoped
| to the loop. Although, I have to point JS started talking about
| making this change almost 20 years ago. As a JS developer, it's
| surprising to me to see Go having to make this change now.
| wheelerof4te wrote:
| Go never should have implemented closures.
|
| You add them and the next thing people want are objects.
| Cthulhu_ wrote:
| And soon enough you get what Javascript ended up with, a half-
| baked implementation of classes and OOP.
|
| Go has structs and functions that work on those structs, good
| enough for me.
| wheelerof4te wrote:
| Same as Python.
|
| It could have been based on tuples and records (similar to
| SQL), but we somehow got typeless OOP.
|
| Wasted opportunity.
| bagful wrote:
| All you really need in an language is the former - closures
| provide encapsulation (instance variables become bound
| variables) and polymorphism (over closures with the same
| function signature) all the same
| hexo wrote:
| If it behaves like that it is not a closure.
| peoplefromibiza wrote:
| a closure can capture either the value or the reference of the
| variables.
|
| in this case it captured the reference which at the time of
| invocation always pointed to the same value.
| kubb wrote:
| Russ Cox and the Go team learned that the loop variable capture
| semantics are flawed not by reflecting about how their language
| works, but through user feedback.
|
| This could have been prevented by having one person on the team
| with actual language design experience, who could point this
| issue out in the design process.
|
| In this case, after 10 or so years, and thousands of production
| bugs, they backpedaled. How many other badly designed features
| exist in the language, and are simply not being acknowledged?
|
| If you point it out, and you're right, will you be heard if you
| don't have a flashy metric to point to, like a billion dollars
| lost?
|
| What if the flaw is more subtle, and explaining why it's bad is
| harder than in this very simple case, that can be illustrated
| with 5 lines of code? What if the link between it and its
| consequences isn't that clear, but the consequences are just as
| grave? Will it ever get fixed?
| AaronFriel wrote:
| Many languages have made this mistake, despite having engineers
| and teams with many decades or centuries of total experience
| working on programming languages. Almost all languages have the
| loop variable semantics Go chose: C/C++, Java, C# (until 5.0),
| JavaScript (when using `var`), Python. Honestly: are there any
| C-like, imperative languages with for loops, that _don't_
| behave like this?
|
| That decision only becomes painful when capturing variables by
| reference becomes cheap and common; that is, when languages
| introduce lightweight closures (aka lambdas, anonymous
| functions, ...). Then the semantics of a for loop subtly
| change. Language designers have frequently implemented
| lightweight closures before realizing the risk, and then must
| make a difficult choice of whether to take a painful breaking
| change.
|
| The Go team can be persuaded, it's just a tall order. And give
| them credit where credit is due: this is genuinely a
| significant, breaking change. It's the right change, but it's
| not an easy decision to make more than a decade into a
| language's usage.
|
| That said, there may be a kernel of truth to what you're
| alluding to: that the Go team can be hard to persuade and has
| taken some principled (I would argue, wrong) positions. I'm
| tracking several Go bugs myself where I believe the Go standard
| library behaves incorrectly. But I don't think this situation
| is the right one to make this argument.
| tuetuopay wrote:
| I hate to be _that_ guy but this would not be possible with
| rust, as the captured reference could not escape the loop
| scope. Either copy the value, or get yelled at the lifetime
| of the reference.
|
| This is one of the things the language was designed to fix,
| by people that looked at the past 50 years or so of
| programming languages, and decided to fix the sorest pain
| points.
| jeremyjh wrote:
| C# made the mistake not when they introduced loops, but when
| they introduced closures, and it didn't become evident until
| other features came along that propelled adoption of
| closures. Go had closures from the beginning and they were
| always central to the language design. C# fixed that mistake
| before the 1.0 release of Go. But the Go team didn't learn
| from it.
| MatthiasPortzel wrote:
| > JavaScript (unless using `for...of`)
|
| The change in Javscript doesn't have anything to do with
| for...of, it's the difference between `var` and `let`. And JS
| made the decision to move to `let` because the semantics made
| more sense before Go was even created (although code and
| browsers didn't update for another several years). That's why
| Go is being held to a higher standard, because it's 10+ years
| newer than the other languages you mentioned.
| AaronFriel wrote:
| Ah, thanks for the correction! That's right. But:
|
| 1. Did JS introduce this change after Go's creation? Yes.
| (And also after C#.)
|
| 2. Did arrow functions support precede let and const
| support? Yes.
|
| Answering the second question and finding the versions with
| support and their release dates answers the first question.
| Arrow functions let and const Firefox 22
| (2013) 44 (2016) Chrome 45 (2015)
| 49 (2016) Node.js 4 (2015) 6 (2016)
| Safari 10 (2016) 10 (2016)
|
| This places it nearly 10 years after the creation of Go.
| And with the exception of Safari, arrow functions were
| available for months to years prior to let and const.
|
| This is somewhat weak evidence for the thesis though; these
| features were part of the same specification (ES6/ES2015),
| but to understand the origin of "let" we also need to look
| at the proliferation of alternative languages such as
| Coffeescript. A fuller history of the JavaScript feature,
| and maybe some of the TC39 meeting minutes, might help us
| understand the order of operations here.
|
| (I'd be remiss not to observe that this is almost an
| accident of "let" as well, there's no intrinsic reason it
| must behave like this in a loop, and some browsers chose to
| make "var" behave like "let". Let and const were originally
| introduced, I believe, to implement lexical scoping, not to
| change loop variable hoisting.)
| adra wrote:
| This isn't a bug in java. Java has the idea of "effectively
| final" variables, and only final or effectively final values
| are allowed to be passed into lambdas seemingly to avoid this
| specific defect. Ironically, I just had a review the other
| day that touched on this go "interaction".
|
| The outcome of this go code in java would be as you'd expect,
| each lambda generated uses a unique copy of the loop
| reference value.
| AaronFriel wrote:
| Oh, today I learned. I think this was an issue in Scala
| (with `var`), but this seems like a great compromise for
| Java core.
|
| I suppose Java had many years after C#'s introduction of
| closures to reflect on what went well and what did not. Go,
| created in 2007, predates both languages having lightweight
| closures. Not surprising that they made the decision they
| did.
|
| Your comment inspired me to ask what Rust does in this
| situation, but of course, they've opted for both a
| different "for" loop construct, but even if they hadn't,
| the borrow checker enforces a similar requirement as Java's
| effectively final lambda limitation.
| ncann wrote:
| Newcomers to Java usually dislike the "Variable used in
| lambda expression should be final or effectively final"
| compiler error, but once you understand why that
| restriction is in place and what happens in other
| languages when there's no such restriction, you start to
| love the subtle genius in how Java did it this way.
| Macha wrote:
| Go, designed between 2007 and 2009, certainly had the
| opportunity to look at their introduction in C# 2.0,
| released 2005, or its syntactic sugar added in C# 3.0,
| released 2007.
| AaronFriel wrote:
| I think that's an ahistorical reading of events. They did
| have the opportunity, but there were very few languages
| doing what Go was at the time it was designed. My
| recollection of the C# 3 to 5 and .NET 3 to 4.5 is a bit
| muddled, but it looks like the spec supports a different
| reading:
|
| C# 3.0 in 2007 introduced arrow syntax. I believe this
| was primarily to support LINQ, and so users were
| typically creating closures as arguments to IEnumerable
| methods, not in a loop.
|
| C# 4.0 in 2010 introduced Task<T> (by virtue of .NET 4),
| and with this it became much more likely users would
| create a closure in a loop. That's how users would add
| tasks to the task pool, after all, from a for loop.
|
| C# 5.0 in 2012 fixes loop variable behavior.
|
| I think the thesis I have is sound: language designers
| did not predict how loops and lightweight closures would
| interact to create error-prone code until (by and large)
| users encountered these issues.
| Dylan16807 wrote:
| > JavaScript (when using `var`)
|
| I would argue that var is an entirely different issue. If
| variables last the entire function then it's far less
| confusing to see closures using the final value. After
| exiting the loop the final value is _right there, still
| assigned to the variable_. You can print it directly with no
| closures needed.
| omoikane wrote:
| This bug appears to be because Go captures loop variables by
| reference, but C++ captures are by copy[1] unless user
| explicitly asked for reference (`&variable`). It seems like
| the same bug would be visually more obvious in C++.
|
| [1] https://eel.is/c++draft/expr.prim.lambda.capture#10
| scottlamb wrote:
| Others have suggested that Rob Pike and Ken Thompson have some
| language design experience, to state it mildly. I also want to
| point out...
|
| > Russ Cox and the Go team learned that the loop variable
| capture semantics are flawed not by reflecting about how their
| language works, but through user feedback.
|
| I think "user feedback" isn't the whole story. It's not just
| the Go team passively listening as users point out obvious
| flaws. I've noticed in other changes (e.g. the monotonic time
| change [1]) the Go team has done a pretty disciplined study of
| user code in Google's monorepo and on github. That's mentioned
| in this case too. This is a good practice, not evidence of
| failure.
|
| [1]
| https://go.googlesource.com/proposal/+/master/design/12914-m...
| kaba0 wrote:
| > Rob Pike and Ken Thompson
|
| They are huge names in the field, but honestly, they just
| suck at language design itself.
| Cthulhu_ wrote:
| Can you substantiate that or is this just flaming?
| scottlamb wrote:
| I'm sure they could come up with a list of language
| decisions they disagree with.
|
| I'm equally sure that if you asked kubb, kaba0, and three
| other strongly opinionated folks for a list of good
| language designers, each of the <5 lists you get back
| would be very short, and there'd be no overlap between
| them.
| mrkeen wrote:
| > If you point it out, and you're right, will you be heard if
| you don't have a flashy metric to point to, like a billion
| dollars lost?
|
| Is this a subtle nod to the billion dollar mistake?
|
| Because they _deliberately_ included the billion dollar mistake
| as part of the language.
|
| Even if they knew better than to include the billion dollar
| mistake, they were probably aware that they couldn't make a
| _popular_ language without including it.
|
| Because most users demand it.
| badrequest wrote:
| Are you suggesting Rob Pike has no experience designing a
| programming language?
| ziyao_w wrote:
| I think the parent was trying to imply that Ken Thompson had
| no experience in designing a programming language :-)
|
| Seriously though, "having experience" and "getting things
| right" are two different things, although Golang got a lot of
| things right, and the parent is being unnecessarily harsh.
| ranting-moth wrote:
| Or Ken Thompson?
| kubb wrote:
| I should say language design knowledge.
| Cthulhu_ wrote:
| Are you aware who these people are?
| randomdata wrote:
| _> Russ Cox and the Go team learned that the loop variable
| capture semantics are flawed not by reflecting about how their
| language works, but through user feedback._
|
| Since "Go 1" was deemed complete and the "Go 2" project began
| in 2018, the direction of the language was given to the
| community. It is no longer the Go team's place to shove
| whatever they think into the language. It has to be what the
| community wants, and that feedback showed it is what they want.
| Cthulhu_ wrote:
| The Go team never shoved anything into the language without
| good reason, and they will not allow the community to shove
| anything into the language; that's how we got half baked
| classes in Javascript and half baked functional programming
| in Java, or the overall trend of languages taking features
| from other languages because community members say "this
| language would be better if it had features from this other
| language" often enough.
| randomdata wrote:
| The "Go 1" project was centred around the Go Team. They
| built what they wanted and needed with little regard for
| the rest of the world. If they felt loop variable capturing
| was important, they would have added it. Of course, they
| didn't find it necessary, so it wasn't added.
|
| When "Go 1" reached its natural stopping point and closed
| down, the "Go 2" project emerged to continue development of
| Go under the wants and needs of the community. That capture
| is being added now because the community has shown a desire
| to have it. The Go Team may use their expertise to guide
| the community in the right direction, but we are here
| because the "Go 2" project is community driven.
|
| The original commenter seems unaware that the project
| changed hands.
| jacquesm wrote:
| > This could have been prevented by having one person on the
| team with actual language design experience
|
| I thought you were serious, right up to that bit. Well played.
| I hope.
| alecbz wrote:
| > This could have been prevented by having one person on the
| team with actual language design experience, who could point
| this issue out in the design process.
|
| Instead of making a mistake, they could have simply not.
|
| See also RFC 9225: Software Defects Considered Harmful
| https://www.rfc-editor.org/rfc/rfc9225.html
| akira2501 wrote:
| > How many other badly designed features exist in the language,
| and are simply not being acknowledged?
|
| Very few.
|
| > If you point it out, and you're right, will you be heard if
| you don't have a flashy metric to point to, like a billion
| dollars lost?
|
| If you're right yet don't have a better idea then what do you
| expect to occur?
|
| > What if the link between it and its consequences isn't that
| clear, but the consequences are just as grave?
|
| The consequence is your developers must be careful with loop
| variables or they will introduce bugs. That's not particularly
| "grave" nor even especially novel.
|
| I'll admit, it's not a good ivory tower language, but then
| again, that's probably why I use it so often. It gets the job
| done and it doesn't waste my time with useless hypothetical
| features.
| [deleted]
| amomchilov wrote:
| I can't find it now, but I remember some joke about "it's an
| interesting language, but why did you ignore the last 50 years
| of programming language design?"
|
| I find Go quite frustrating in how it decries how over-
| complicated some features are, and slowly comes around to
| realize that oh, maybe people designed them for a reason (who
| woulda thunk it?).
| Patrickmi wrote:
| So whats your point ?, old ideas never die ?, language design
| is not language purpose and goal ?, they made a mistake
| creating Go ?, refusing to find something suitable or just
| break compatibility?
| rsc wrote:
| I answered some of this above:
| https://news.ycombinator.com/item?id=37577312
| dangoodmanUT wrote:
| Thank god!!!
| up2isomorphism wrote:
| Go is such a weird language in a sense it is so opinionated and
| so non opinionated at the same time.
| wwarner wrote:
| Feel a great sense of relief reading this. This fixes the single
| biggest wart in Go.
| darren0 wrote:
| Next they can fix nil checks on interfaces.
| cyphar wrote:
| Unfortunately, the distinction is sometimes (though rarely)
| useful and would be a far more disruptive change than this
| one. I think you would actually need a Go v2 to change it.
| maccard wrote:
| No, the biggest wart is error handling. foo,
| err := getFoo() if err != nil ... bar, err
| := getBar() fmt.Println(bar)
|
| Misses an error check on getBar. Scoping rules mean that
| if foo, err := getFoo(); err != nil
|
| Gets untenable with nesting fairly quickly.
|
| It also introduces invalid states - what does getFoo return if
| it returns an error too. Do we change the API to return a
| pointer and return nil, or do we have a partially constructed
| object that is in an invalid state?
| raydiatian wrote:
| Can somebody please explain to me why this doesn't constitute a
| major version due to a breaking change? Maybe I didn't read
| precisely enough but it sure sounds like a breaking semantic,
| esp. with the fact that "this will only work for versions 1.22
| and later." Sounds like a version upgrade trap to me? What am I
| missing?
|
| Or is it just because it's Golang and they're "we'll never
| release a go v2 even if we actually do release go v2 and call it
| v1.x"
| Cthulhu_ wrote:
| In practice it won't break anything, unless there is code that
| accidentally relies on this behaviour. Most of the code
| affected by this will already have a workaround - the `x := x`
| mentioned - which can be removed after applying this change.
| ben0x539 wrote:
| Seems like a pragmatic decision where the breakyness of the
| change is mitigated by the module versioning thing. Old code
| gets the old behavior, code written in newly created or updated
| modules gets the new behavior. Everybody is happy, compared to
| the alternative where this ships in a mythical go v2 which
| nobody uses while this sort of bug keeps sneaking people's
| actual work.
| raydiatian wrote:
| Thanks, must have missed the module versioning bit
| fyzix wrote:
| This will result in more memory allocations but it's well worth
| it.
| icholy wrote:
| There's no reason it can't be optimized away when they're not
| necessary.
| orblivion wrote:
| For migrating, I wonder if there are any tools that could, let's
| say, go through your codebase and add a "// TODO - check" to
| every place that might be affected.
| [deleted]
| Cthulhu_ wrote:
| I know for previous code changes they had a tool that would
| just do a find & replace for you.
|
| But you're describing a linter, which just outputs a line on
| your terminal with a warning; I wouldn't want a tool like that
| to add churn and tasks to my codebase (even though I'm guilty
| of adding TODOs myself and leaving them for years because
| ultimately they're not important enough)
| orblivion wrote:
| Well automatic changes would be even better I just assumed
| that a tool can't infer your intentions well enough for this
| one.
| tgv wrote:
| I think there's a linter for it, perhaps more than one, in
| golangci-lint. It might be exportloopref and/or loopclosure.
| winwhiz wrote:
| I hope this experiment fails. It is one cherry picked do-what-I-
| mean feature that muddies the Go1 Promise and will almost always
| be covering up a subtle logic error. It also adds a bit more
| historical knowledge you have to remember: In early part of the
| second thousand twenty fourth year of our Lord, in version
| 1.22.0, a subtle change was made that maybe ignored on a per file
| basis or per module basis as has been done previously in the
| future as you might recall in 1.20.8 and 1.19.13.
|
| If it doesn't fail I have a couple more ideas, if the compiler
| can prove my double only ever interacts with ints then... just do
| what I mean it's provably correct.
|
| package main // YUM! I WANT MOAR DWIMMY.... AND SIGILS AND BLESS
| AND UNLESS
|
| func easy(one int, won float64) { print(one + won) }
|
| func main() { easy(1, 1) }
| AaronFriel wrote:
| The C# language team encountered this as well, after introducing
| lightweight closures in C# 4.0 it quickly became apparent that
| this was a footgun. Users almost always used loop variables
| incorrectly, and C# 5.0 made the breaking change.
|
| Eric Lippert has a wonderful blog on the "why" from their
| perspective: https://ericlippert.com/2009/11/12/closing-over-the-
| loop-var...
|
| I had a bit of trouble finding the original C# 5 announcement;
| that's hopefully not been lost in the (several?) blog migrations
| on the Microsoft domain since 2012.
| hinkley wrote:
| Java also had this problem with anonymous classes. The solution
| is usually to introduce a functor. Being pass-by-value, it
| captures the state of the variables at its call time, which
| helps remove some ambiguity in your code.
|
| If you try to do something weird with variable capture, then
| any collections you accumulate data into (eg, for turning an
| array into a map), will behave differently than any declared
| variables.
|
| Go is trying to thread the needle by only having loop counters
| work this way. But that still means that some variables act
| weird (it's just a variable that tends to act weird anyway).
| And I wonder what happens when you define multiple loop
| variables, which people often do when there will be custom
| scanning of the inputs.
| simiones wrote:
| Java has never had this problem with variables (either in a
| for loop or free-floating ones), since Java has never had
| support for closures.
|
| There is one somewhat similar problem in Java that you're
| maybe thinking of: anonymous classes that reference fields of
| the current object. I don't think that behavior is
| surprising, and there are very important use cases for it.
|
| What Go is doing is perfectly sensible. The ability to
| capture variables is extremely powerful, and often desired.
| It's just the unexpected scoping of loop variables that
| introduces a problem. The following code is doing exactly
| what most people would expect, for example: a
| := 0 incr := func() {a += 1} print := func()
| {fmt.Printf("%d", a)} print() // prints 0 incr()
| print() //prints 1
| kaba0 wrote:
| > An anonymous class cannot access local variables in its
| enclosing scope that are not declared as final or effectively
| final.
|
| So no, Java didn't have this problem.
| eru wrote:
| Which of the various meanings of the word 'functor' are you
| using here?
| hinkley wrote:
| Function that returns a function.
|
| You pass your counter into the function, it returns a
| function that remembers the original value, not the value
| as it keeps iterating later on in the caller.
| eru wrote:
| That's just a higher order function?
|
| That's an interesting definition. I thought you would
| either go with https://en.wikipedia.org/wiki/Functor_(fun
| ctional_programmin... or with
| https://en.wikipedia.org/wiki/Function_object
|
| https://en.wikipedia.org/wiki/Functor_(disambiguation)
| has a few more choices, but doesn't seem to have yours.
| Sharlin wrote:
| Functor must be one of the worst overloaded terms in all
| of computing.
| [deleted]
| nerdponx wrote:
| Meanwhile Python has received this same feature request many
| times over the years, and the answer is always that it would
| break existing code for little major benefit
| https://discuss.python.org/t/make-lambdas-proper-closures/10...
|
| Given how much of an uproar there was over changing the string
| type in the Python 2 -> 3 transition, I can't imagine this
| change would ever end up in Python before a 4.0.
|
| Cue someone arguing about how bad Python is because it won't
| fix these things, and then arguing about how bad Python is
| because their scripts from 2003 stopped working...
| travisd wrote:
| It's worth noting that it's much less of a problem in Python
| due to the lack of ergonomic closures/lambdas. You have to
| construct rather esoteric looking code for it to be a
| problem. add_n = [] for n in
| range(10): add_n.append(lambda x: x + n)
| add_n[9](10) # 19 add_n[0](10) # 19
|
| This isn't to say it's *not* a footgun (and it has bit me in
| Python before), but it's much worse in Go due to the
| idiomatic use of goroutines in a loop: for
| i := 0; i < 10; i++ { go func() {
| fmt.Printf("num: %d\n", i) }() }
| o11c wrote:
| It's actually _worse_ in Python since there 's no support
| for variable lifetimes within a function, so the `v2`
| workaround is still broken. (the default-argument
| workaround "works" but is scary)
|
| This makes it clear: the underlying problem is NOT about
| for loops - it's closures that are broken.
| nerdponx wrote:
| It's not _broken_ , it's a different design. Maybe worse
| in a lot of cases, but it's not broken. It's working as
| intended.
| eru wrote:
| You could say the design is broken, but the
| implementation is working as intended by the design.
| simiones wrote:
| I don't think anyone is puzzled by the Go snippet being
| wrong.
|
| The bigger problem in Go is the for with range loop:
| pointersToV := make([]*val, len(values)) for i, v :=
| range values { go func() { fmt.Printf("num: %v\n",
| v) } () //race condition pointersToV[i] = &v //will
| contain len(values) copies of a pointer to the last item in
| values }
|
| This is the one they are changing.
|
| Edit: it looks like they're actually changing both of
| these, which is more unexpected to me. I think the C#
| behavior makes more sense, where only the foreach loop has
| a new binding of the variable in each iteration, but the
| normal for loop has a single lifetime.
| Spivak wrote:
| Ignoring the strange nature of this code in the first place
| the more pythonic way to do it would be
| from functools import partial from operators import
| add add_n = [partial(add, n)) for n in
| range(10)] assert add_n[5](4) == 9
|
| Look ma, no closures.
| eru wrote:
| 'partial' creates a closure for you.
| Spivak wrote:
| Unless you're talking philosophically how classes and
| closures are actually isomorphic then no, it doesn't.
| None of the variables in the outer scope are captured in
| the class instance.
|
| https://github.com/python/cpython/blob/main/Lib/functools
| .py...
|
| Here's a simplified version of that code that
| demonstrates the pattern. class
| partial: def __init__(self, func, *args,
| **kwargs): self.func = func
| self.args = args self.kwargs = kwargs
| def __call__(self, *args, **kwargs): return
| self.func(*self.args, *args, **(self.kwargs | kwargs))
| p = partial(add, 5) # -> an instance of partial with
| self.args = (5,) res = p(4) # -> calls
| __call__ which merges the args and calls add(5, 4)
| eru wrote:
| I was talking 'philosophically' in that sense. The
| partial object does create a new scope that binds a few
| of those variables.
|
| But you are also right that the mechanisms in Python are
| different (on some suitable mid-level of abstraction) for
| those two.
| hinkley wrote:
| Everyone else solved this problem by using list
| comprehensions instead. Rob has surely heard of those.
| lmm wrote:
| Of the two comprehension syntaxes in Haskell, Python
| picked the wrong one. Do notation (or, equivalently,
| Scala-style for/yield) feels much more consistent and
| easy to use - in particular the clauses are in the same
| order as a regular for loop, rather than the middle-
| endian order used by list comprehensions.
| eru wrote:
| Haskell has both do-notation and list comprehension.
|
| Comprehension in both Python and Haskell (for both lists
| and other structures) use the same order in both
| language, as far as I remember.
| lmm wrote:
| > Haskell has both do-notation and list comprehension.
|
| Right, and do-notation is the one everyone uses, because
| it's better. Python picked the wrong one.
|
| > Comprehension in both Python and Haskell (for both
| lists and other structures) use the same order in both
| language, as far as I remember.
|
| It may be the same order as Haskell but it's a terrible
| confusing order. In particular if you want to go from a
| nested list comprehension to a flat one (or vice versa)
| then you have to completely rearrange the order it's
| written in, whereas if you go from nested do-blocks to
| flat do-blocks then it all makes sense.
| nerdponx wrote:
| But Python doesn't have any concept of a monad, so what
| would do-notation even be in Python? And who is the
| "everyone" using do-notation? I don't see any analogous
| syntax in Lua, Javascript, Ruby, or Perl.
|
| In Python there is a nice tower of abstractions for
| iteration, but nothing more general than that, so it
| makes perfect sense IMO to use the syntax that directly
| evokes iteration.
|
| The existing syntax is meant to mirror the syntax of a
| nested for loop. I agree that maybe it's confusing, but
| if you want to go from a multi-for comprehension to an
| actual nested for loop, then you _don 't_ have to invert
| the order.
| lmm wrote:
| > But Python doesn't have any concept of a monad, so what
| would do-notation even be in Python?
|
| It could work on the same things that Python's current
| list comprehensions work on. I'm just suggesting a
| different syntax. Comprehensions in Haskell originally
| worked for all monads too.
|
| > And who is the "everyone" using do-notation? I don't
| see any analogous syntax in Lua, Javascript, Ruby, or
| Perl.
|
| I meant that within Haskell, everyone uses do notation
| rather than comprehensions.
|
| > The existing syntax is meant to mirror the syntax of a
| nested for loop. I agree that maybe it's confusing, but
| if you want to go from a multi-for comprehension to an
| actual nested for loop, then you don't have to invert the
| order.
|
| You have to invert half of it, which I find more
| confusing than having to completely invert it. do-
| notation style syntax (e.g. Scala-style for/yield) would
| keep the order completely aligned.
| eru wrote:
| I see what you mean, but I don't find the order that
| confusing in neither Haskell or Python.
|
| However, I can imagine a feature that we could add to
| Python to fix this: make it possible for statements to
| have a value. Perhaps something like this:
| my_generator = \ for i in "abc":
| for b in range(3): print("foo")
| yield (i, b)
|
| or perhaps have the last statement in block be its value
| (just like Rust or Ruby or Haskell do with the last
| statement in a block), and make the value of a for-loop
| be a generator of the individual values:
| my_list = list( for i in "abc": for
| b in range(3): (i, b))
|
| Though there's a bit of confusion here whether the latter
| examples should be a flat structure or a nested one. You
| could probably use a similiar mechanism as the existing
| 'yield from' to explicitly ask for the flat version, and
| otherwise get the nested one: my_list =
| list( for i in "abc": yield from
| for b in range(3): (i, b))
|
| Making Python statements have values looks to me like the
| more generally useful change than tweaking
| comprehensions. You'd probably not need comprehension at
| all in that case. Especially since you can already write
| loop header and body on a single line like
| for i in range(10): print(i)
|
| if they are short enough.
| nerdponx wrote:
| for i in range(10): print(i)
|
| But what would that return? [None] * 10?
|
| The limited whitespace-based syntax limits the potential
| for fun inline statement things, but it also completely
| dodges the question of what any particular statement
| should evaluate to when used as an expression.
| camgunz wrote:
| When I was starting in Python years ago I had to turn my
| brain inside out to learn how to write list
| comprehensions. Sometimes I wonder what it's like to be a
| normal person with a normal non-programmer brain, having
| forgotten it entirely these last many years.
| [deleted]
| panzi wrote:
| How does list comprehension change anything here? This
| has the same problem: add_n = [lambda
| x: x + n for n in range(10)] add_n[9](10) # 19
| add_n[0](10) # 19
| billyjmc wrote:
| I'm not sure what they mean by list comprehensions,
| either, but for completeness's sake, I must point out
| that this is solvable by adding `n` as a keyword argument
| defaulting to `n`: add_n = [lambda x,
| n=n: x + n for n in range(10)] add_n[9](10) # 19
| add_n[0](10) # 10
| drekipus wrote:
| This is the way
|
| Also pylsp warns you about this
| jshen wrote:
| Doesn't go vet complain about your code? I'm not at my
| computer right now so can't check.
| simiones wrote:
| Why would it? It's perfectly correct code, it's just not
| doing what you'd expect.
|
| It might complain about the race condition, to be fair,
| but the same issue can be reproduced without goroutines
| and it would be completely correct code per the
| semantics.
| jshen wrote:
| In many languages "if x = 3" is perfectly valid code, but
| almost certainly not what the person intended "if x ==
| 3". It's very smart to warn someone in a scenario like
| this.
| _ikke_ wrote:
| > Tools have been written to identify these mistakes, but
| it is hard to analyze whether references to a variable
| outlive its iteration or not. These tools must choose
| between false negatives and false positives. The
| loopclosure analyzer used by go vet and gopls opts for
| false negatives, only reporting when it is sure there is
| a problem but missing others.
|
| So it will warn in certain situations, but not all of
| them
| Frotag wrote:
| I've run into this once. IIRC the workaround was to add a
| n=n arg to the lambda
| eru wrote:
| In Python you are much more likely to hit that problem not
| with closures constructed with an explicit 'lambda', but
| with generator-comprehension expressions.
| (((i, j) for i in "abc") for j in range(3))
|
| The values of the above depends on in which order you
| evaluate the whole thing.
|
| (Do take what I wrote with a grain of salt. Either the
| above is already a problem, or perhaps you also need to mix
| in list-comprehension expressions, too, to surface the
| bug.)
| nerdponx wrote:
| Yeah, this one is weird: gs1 = (((i, j)
| for i in "abc") for j in range(3)) gs2 = [((i, j)
| for i in "abc") for j in range(3)]
| print(list(map(list, gs1))) print(list(map(list,
| gs2)))
|
| Results: [[('a', 0), ('b', 0), ('c', 0)],
| [('a', 1), ('b', 1), ('c', 1)], [('a', 2), ('b', 2),
| ('c', 2)]] [[('a', 2), ('b', 2), ('c', 2)], [('a',
| 2), ('b', 2), ('c', 2)], [('a', 2), ('b', 2), ('c', 2)]]
|
| That's a nice "wat" right there. I believe the
| explanation is that in gs2, the range() is iterated
| through immediately, so j is always set to 2 before you
| have a chance to access any of the inner generators.
| Whereas in gs1 the range() is still being iterated over
| as you access each inner generator, so when you access
| the first generator j=1, then j=2, etc.
|
| Equivalents: def make_gs1():
| for j in range(2): yield ((i, j) for i in
| "abc") def make_gs2(): gs = []
| for j in range(2): gs.append(((i, j) for i
| in "abc")) return gs
|
| Late binding applies in both cases of course, but in the
| first case it doesn't matter, whereas in the latter case
| it matters.
|
| I think early binding would produce the same result in
| both cases.
| _ZeD_ wrote:
| or you could just be eager and use lists:
| >>> [[(i, j) for i in "abc"] for j in range(3)]
| [[('a', 0), ('b', 0), ('c', 0)], [('a', 1), ('b', 1),
| ('c', 1)], [('a', 2), ('b', 2), ('c', 2)]]
| nerdponx wrote:
| Right, creating generators in a loop is not usually
| something you want to do, but it's meant to demonstrate
| the complexity that arises from late binding rather than
| demonstrate something you would actually want to do in a
| real program.
| sneak wrote:
| Somehow, Go managed to not break old code and also fix the
| problem.
|
| I think this is a good case of Python not fixing things,
| given that a fix exists that solves both problems.
| baq wrote:
| Python probably could change this with a from __future__
| import, i.e. in the same way.
| kazinator wrote:
| If the change /doesn't/ break old code, it's also poorly
| justified.
|
| It means code doesn't care about the issue being addressed.
|
| The feature is only justified if it changes existing code,
| such that bugs you didn't even know about are fixed.
|
| I.e. people read about the issue, investigate their code
| bases and go, oh hey, we actually have a latent bug here
| which the change will fix.
| esrauch wrote:
| New code written today will use the new version and have
| the correct behavior from day 1.
|
| Old code that is maintained will eventually be upgraded,
| which yes does come with work sometimes where you realize
| your code works on version X but not version X+10 and you
| do a combination of tests and reading patch notes to see
| what changed.
| kazinator wrote:
| There is no "correct" behavior here; either one is a
| valid choice that can be documented and that programs can
| rely on and exploit.
|
| Code doesn't care about when it's written, only what you
| run it on, and with what compatibility options.
|
| E.g. one possibility is that ten-year-old code that
| wrongly assumed the opposite behavior, and has a bug,
| will start to work correctly on the altered
| implementation.
| jjnoakes wrote:
| There's a second motivation in my opinion. Code might
| work today without the change, but it could be because
| the author originally wrote buggy code, caught it in
| testing, and had to waste time tracking it down and
| understanding nuances that don't need to be there. Once
| they figured that out, they implemented an ugly
| workaround (adding an extra function parameter to a
| goroutine or shadowing the loop variable with n := n).
|
| Good language designers want to avoid both wasting
| developer's time and requiring ugly workarounds. Making a
| change that does both, especially if it doesn't break old
| code, is great imo.
| kazinator wrote:
| Whichever way you implement the semantics of the loop
| variable, the developer has to understand the nuances,
| don't you think? And those nuances have to be there; all
| you can do is replace them with other nuances.
|
| If a fresh variable _i_ is bound for each iteration, then
| an _i++_ statement in the body will not have the intended
| effect of generating an extra increment that skips an
| iteration.
|
| If you want the _other_ semantics, whichever one that is,
| the workaround is ugly.
| wrboyce wrote:
| By letting you specify a language version requirement? Not
| exactly backwards compatible (because it is explicitly not,
| as per the article).
|
| Python doesn't make breaking changes in non-major versions,
| so as mentioned by the upthread comment the appropriate
| place for this change would be in Python 4.
|
| Given the above, I'm really not sure what point you think
| you're making in that final paragraph.
| AlphaSite wrote:
| They do and have made relatively small ones, e.g.
| promoting __future__ features to default, etc.
| carbotaniuman wrote:
| This seems weird to given the number of breakages and
| standard library changes I seem to run into every
| version.
| wrboyce wrote:
| Really? I find that surprising. I don't write as much
| code as I used to but I've been writing Python for a long
| time and the only standard library breakages that come to
| mind were during the infamous 2 -> 3 days.
|
| What sort of problems are have you faced upgrading minor
| versions?
| nayuki wrote:
| One of the few that I remember is fractions.gcd() ->
| math.gcd().
|
| See https://docs.python.org/3.0/library/fractions.html#fr
| actions... , https://docs.python.org/3.5/library/fraction
| s.html#fractions... ,
| https://docs.python.org/3.9/library/fractions.html
| (gone),
| https://docs.python.org/3.5/library/math.html#math.gcd
| orbisvicis wrote:
| The docs are full of remarks like "removed in 3.0 and
| reintroduced in 3.4" or "deprecated in 3.10", etc. A big
| one is the removal of the loop parameter in asyncio, but
| a lot of asyncio internals are (still?) undergoing
| significant changes, as getting the shutdown behavior
| correct is surprisingly difficult. Personally it's never
| cause me any issues - I'm always on board with the
| changes.
| nerdponx wrote:
| Asyncio was explicitly marked as provisional for years
| and most of the incompatible changes happened during that
| time. Same goes for typing. The rest of the language is
| very very stable.
| pcl wrote:
| _> To ensure backwards compatibility with existing code,
| the new semantics will only apply in packages contained in
| modules that declare go 1.22 or later in their go.mod
| files._
| IshKebab wrote:
| Python could very easily have a similar mechanism. Hell
| even CMake manages to do this right, and they got "if"
| wrong.
|
| The Python devs sometimes seem stubbornly attached to
| bugs. Another one: to reliably get Python 3 on Linux and
| Mac you have to run `python3`. But on Windows there's no
| `python3.exe`.
|
| Will they add one? Hell no. It might confuse people or
| something.
|
| Except... if you install Python from the Microsoft Store
| it _does_ have `python3.exe`.
| beeburrt wrote:
| > to reliably get Python 3 on Linux and Mac you have to
| run `python3`.
|
| This is not true on my Fedora 38 system, same with
| current Kali linux. Although, it is the case with Ubuntu
| 22.04.3.
| IshKebab wrote:
| What do you mean? Fedora 38 doesn't have `python3`? Are
| you sure?
| wnoise wrote:
| _reliably_, as in on the vast majority of machines.
| billyjmc wrote:
| Is that really python's fault? It seems like it's the
| distro making a design decision.
| wnoise wrote:
| Well, no, not python's fault -- clearly the distros', and
| they probably should be blamed. But a PEP saying python2
| and python3 should invoke the correct interpreter would
| help motivate the distributions.
|
| (This is isomorphic to the usual victim-blaming
| discussion. Fault and blame vs some ability to make a
| difference; it's a shame that correctly pointing out a
| better strategy is both used to attack victims and
| attacked for attacking victims in the cases when that
| wasn't intended.)
| wrboyce wrote:
| I've not run "python3" in years on my Mac, and I'm almost
| certain I never type it into Linux machines either;
| either I'm losing my mind, or there are some ludicrous
| takes in this thread.
| rat9988 wrote:
| You are surely losing your mind then. Python3 isn't
| something esoteric.
| wrboyce wrote:
| Entirely possible, but my point was I just type "python"
| and Python 3 happens. Do modern OS even come with Python
| 2 anymore?
|
| I'm not claiming any mystery about Python, just disputing
| how the modern version is invoked.
| lmm wrote:
| > I just type "python" and Python 3 happens.
|
| That was the old way. Python now recommends against
| installing Python3 in a way that does that, and most
| modern *nix don't.
| wrboyce wrote:
| I suspect my confusion stemmed from mostly invoking
| `ipython` which doesn't include the 3 suffix (ok, part of
| the confusion may've been pub-related too :D).
| erik_seaberg wrote:
| 13.5.2 has /usr/bin/python3 (it's 3.9.6) but not python2
| or just python. Not sure when they changed, and YMMV with
| Homebrew.
| jlokier wrote:
| Just tried "python" and "python3" on various Linux
| distros, which output respectively:
|
| On an Ubuntu 20.04 desktop VM: python =>
| Python 2.7.18 (default, Jul 1 2022, 12:27:04)
| python3 => Python 3.8.10 (default, May 26 2023, 14:05:08)
|
| On an Ubuntu 19.04 server: python =>
| -bash: python: command not found python3 => Python
| 3.7.5 (default, Apr 19 2020, 20:18:17)
|
| On an Ubuntu 20.10 server: python =>
| -bash: python: command not found python3 => Python
| 3.8.10 (default, Jun 2 2021, 10:49:15)
|
| I no longer have access to some RHEL7 and RHEL8 machines
| used for work recently, but if I recall correctly they do
| this by default:
|
| Red Hat Enterprise Linux 7: python =>
| Some version of Python 2 python3 => Some version of
| Python 3
|
| Red Hat Enterprise Linux 8: python =>
| -bash: python: command not found # (use "python2" for
| Python 2) python3 => Some version of Python 3
|
| You can change the default behaviour of unversioned
| "python" to version 2 or 3 on all the above systems, I
| think, so if you're running a Linux distro when "python"
| gets you Python 3, that configuration might have been
| done already.
|
| MacOS 10.15 (Catalina) does something interesting:
| python => WARNING: Python 2.7 is not recommended.
| This version is included in macOS for compatibility with
| legacy software. Future versions of
| macOS will not include Python 2.7.
| Instead, it is recommended that you transition to using
| 'python3' from within Terminal. Python
| 2.7.16 (default, Jun 5 2020, 22:59:21) python3 =>
| Python 3.8.2 (default, Jul 14 2020, 05:39:05)
| turboponyy wrote:
| Depending on the package manager / distribution, 'python'
| might be symlinked to either Python 2 or Python 3. If you
| don't have Python 3 installed, it might very well point
| to Python 2. These days it will almost certainly prefer
| Python 3, but I am also in the habit of actually typing
| 'python3' instead of 'python' because of what I assume
| are issues I've had in the past.
| rfoo wrote:
| > Except... if you install Python from the Microsoft
| Store it does have `python3.exe`.
|
| It's worse. If you _don 't_ install Python from the
| Microsoft Store there will still be a `python3.exe`. But
| running it just opens Microsoft Store.
|
| Imagine how confused one could be when someone typed
| `python3 a.py` over a SSH session and nothing happened.
| nerdponx wrote:
| Right, and Go has the luxury of being a compiler that
| generates reasonably portable binaries, while Python
| requires the presence of an interpreter on the system at
| run time.
| josephg wrote:
| The same trick would work with python just as well.
| There's nothing about Python's status as an interpreter
| which would stop them from adding a python semantic
| version somewhere in each python program - either in a
| comment at the top of each source file or in an adjacent
| config file. The comment could specify the version of
| python's semantics to use, which would allow people to
| opt in to new syntax (or opt out of changes which may
| break the code in the future).
|
| Eg # py 3.4
| Cthulhu_ wrote:
| Yeah, it would just mean that the interpreter - just like
| the Go compiler - would need to have the equivalent of
| "if version > 3.4 do this, else do that". Which is fine
| for a while, but I can imagine it adds a lot of
| complexity and edge cases to the interpreter / compiler.
|
| Which makes me think that a Go 2.0 or Python 4 will
| mainly be about removing branches and edge cases from the
| compiler more than making backwards-incompatible language
| changes.
| josephg wrote:
| This is the direction multiple languages are moving in.
| Go and Rust both have something like this. (In Rust
| they're called "editions"). I think its inevitable that
| compilers get larger over time. I hope most of the time
| they aren't too onerous to maintain - there aren't _that_
| many spec breaking changes between versions. But if need
| be, I could also imagine the compiler eventually
| deprecating old versions if it becomes too much of an
| issue.
|
| Arguably C & C++ compilers do the same thing via -std=c99
| and similar flags at compile time.
|
| Anyway, nothing about this is special or different with
| python. I bet the transition to python 3 would have been
| much smoother if scripts could have opted in (or opted
| out) of the new syntax without breaking compatibility.
| lloeki wrote:
| > Python requires the presence of an interpreter on the
| system at run time.
|
| A runtime interpreter does not prevent Perl to do similar
| things via `use 5.13`
|
| Python has `from future` with similar abilities, it would
| absolutely be possible to do the same as Perl and Go and
| fix what needs to be fixed without breaking old code. One
| could design a `import 3.22` and `from 3.22 import
| unbroken_for` and achieve the same thing.
| masklinn wrote:
| Python is in a larger bind because it only has function
| scoping _and_ variable declaration is implicit. It does not
| _have_ sub-function scopes.
|
| So does not really have a good way to fix the issue, even by
| using a different keyword as JS did.
|
| OTOH default parameters being evaluated at function
| definition make mitigating it relatively simple.
| codeflo wrote:
| Yeah, block scoping is one of those "weird CS ideas" that
| I'm sure at some point early in Python's design was deemed
| too complicated for the intended audience, but is also
| quite a natural way to prevent some human errors.
| JavaScript made the same mistake and later fixed it
| (let/const).
| kzrdude wrote:
| I love python, but it's one of the biggest annoyances.
| Local variables like in Lua make a lot of sense.
| Cthulhu_ wrote:
| I'm not a computer scientist so I can't rule whether
| function scope was a mistake, and can't see how block
| scoping would be considered too complicated, I personally
| think it fits much better with my mental model. Then
| again, Python doesn't have blocks in the traditional
| sense of the word IIRC, in C style languages the
| accolades are a pretty clear delineator.
|
| Parts of my previous job were terrible because it had JS
| functions thousands of lines of code long where variables
| were constantly reused (and often had to be unset before
| another block of code). That said, that wasn't the fault
| of function scope per se, but of a bad but very
| productive developer.
| masklinn wrote:
| TBF you can have block scoping in an indentation-based
| language, though it probably help to merge the too, as in
| Haskell: `let...in` will define variables in the `let`
| clause, and those variables are only accessible in the
| `in` clause (similarly case...of)
| arnsholt wrote:
| Python does actually have a single instance of sub-function
| scopes: When you say `try: ... except Exception as e: ...`
| the `e` variable is deleted at the end of the `except`
| clause. I think this is because the exception object, via
| the traceback, refers to the handling function's invocation
| record, which in turn contains a map of all the function's
| local variables. So if the variable worked like normal
| variables in Python it'd create a reference cycle and make
| the Python GC sad. So if you need that behaviour, you need
| to reassign the exception to a new name [0].
|
| 0: https://docs.python.org/3/reference/compound_stmts.html#
| the-...
| orbisvicis wrote:
| Is it a bug? I've always depended on late-binding closures
| and I think even recently in a for loop, not that I'm going
| to go digging. You can do neat things with multiple functions
| sharing the same closure. If you don't want the behavior bind
| the variable to a new name in a new scope. From the post I
| get the sense that this is more problematic for languages
| with pointers.
| lmm wrote:
| IMO it's a misdesign in the same way as e.g. JavaScript's
| "this". Most languages figured out 40 or so years ago that
| scoping should be lexical.
| orbisvicis wrote:
| The scope is lexical, the lookup is dynamic. What you
| want is for each loop iteration to create a new scope,
| which I would categorize as "not lexical".
| lmm wrote:
| By that argument a recursive function shouldn't create a
| new scope every time it recurses, and a language that
| fails Knuth's 1964 benchmark of reasonable scoping (the
| "man or boy test") would be fine. The loop body is
| lexically a block and like any other block it should have
| its own scope every time it runs.
| orbisvicis wrote:
| Except that the for loop does not create a new scope and
| is not a block:
|
| https://docs.python.org/3/reference/executionmodel.html#s
| tru...
| Dylan16807 wrote:
| Also, loop bodies _already did_ have their own scope each
| iteration.
|
| I wouldn't say either behavior is non-lexical. The only
| thing changing is which lexical scope these variables go
| into.
| lmm wrote:
| If the loop "variable" (and IMO thinking of it as a
| variable is halfway to making the mistake) is in a single
| scope whose lifetime is all passes through the loop body,
| that's literally non-lexical; there is no block in the
| program text that corresponds to that scope. Lexically
| there's the containing function and the loop body,
| there's no intermediate scope nestled between them.
| Dylan16807 wrote:
| > and IMO thinking of it as a variable is halfway to
| making the mistake
|
| I used plural for a reason.
|
| > there is no block in the program text that corresponds
| to that scope.
|
| The scope starts at the for. There is a bunch of state
| that is tied to the loop, and if you rewrote it as a less
| magic kind of loop you'd need to explicitly mark a scope
| here.
|
| What's non-lexical about it? You could replace "for" with
| "{ for" to see that a scope of "all passes through the
| loop body" does not require anything dynamic.
|
| And surely whether a scope is implicit or explicit
| doesn't change whether a scope is lexical. In C I can
| write "if (1) int x=2;" and that x is scoped to an
| implicit block that ends at the semicolon.
|
| Would you say an if with a declaration in it is non-
| lexical, because both the true block and the else block
| can access the variable? I would just say the if has a
| scope, and there are two scopes inside it, all lexical.
| And the same of a for loop having an outer and inner
| scope.
| simiones wrote:
| The problem isn't with closures, the closure semantics are
| perfectly fine.
|
| The problem is in the implementation of for-range loops,
| where the clear expectation is that the loop variable is
| scoped to each loop iteration, not to the whole loop scope
| (otherwise said, that the loop variable is re-bound to a
| new value in each loop iteration). The mental mode
| approximately everyone has for a loop like this:
| for _, v := range values { //do stuff with v
| }
|
| is that it is equivalent to the following loop:
| for i := range values { v := values[i] //do
| stuff with v }
|
| In Go 1.22 and later, that is exactly what the semantics
| will be.
|
| In Go 1.21 or earlier, the semantics are closer to this
| (ignoring the empty list case for brevity):
| for i := 0, v := values[0]; i < len(values); i++,
| v=values[i] { //do stuff with v }
|
| And note that this mis-design has appeared in virtually
| every language that has loops and closures, and has been
| either fixed (C# 5.0, Go 1.22) or it keeps being a footgun
| that people complain about (Python, Common Lisp, C++).
| orbisvicis wrote:
| I think that's a C-centric assumption which is moot as
| Python's "for" does not create _any_ new scopes. Just
| reading Knuth 's man-or-boy test I was struck by the
| alien nature of the ALGOL 60 execution model, even though
| to Python it can be considered a distant ancestor.
|
| https://en.m.wikipedia.org/wiki/Man_or_boy_test
| 4bpp wrote:
| I don't know, my feeling is that the issue really is with
| how closure capture was interpreted when imperative
| languages started implementing lambdas. What was
| happening in Go seems to either amount to default capture
| by reference rather than value, or to the loop counters
| in question being unmarked reference types. The former
| strikes me as unintuitive given that before lambdas,
| reference-taking in imperative languages was universally
| marked (ex. &a); the latter strikes me as unintuitive
| because with some ugly exceptions (Java), reference
| _types_ should be marked in usage (ex. *a + *b instead of
| a+b). Compare to C++ lambdas, where reference captures
| must be announced in the [] preamble with the & sigil
| associated with reference-taking.
|
| (In functional languages, this problem did not arise,
| since most variables are immutable and those that are not
| are syntactically marked and decidedly second-class. In
| particular, you would probably not implement a loop using
| a mutable counter or iterator.)
| simiones wrote:
| Even if Go allowed both capture-by-value and capture-by-
| reference, this issue would have arisen when using
| capture-by-reference.
|
| For example, in the following C++: auto v
| = std::vector<int>{1, 2, 3}; auto prints =
| std::vector<std::function<void()>>(); auto incrs =
| std::vector<std::function<void()>>(); for (auto x :
| v) { prints.push_back([&x]()->void
| {std::cout<<x<<", "; })
| incrs.push_back([&x]()->void {++x;}); } for
| (auto f : incrs) { f(); } for (auto f
| : prints) { f(); } //expected to print 2,
| 3, 4; actually prints 6, 6, 6
|
| I would also note that this problem very much arises in
| functional languages - it exists in the same way in
| Common Lisp and Scheme, and I believe it very much
| applies to OCaml as well (though I'm not sure how their
| loops work).
|
| Tried it out, OCaml does the expected thing:
| open List let funs = ref [ ] ;; for i = 1 to
| 3 do funs := (fun () -> print_int i) :: !funs
| done ;; List.iter (fun f -> f()) !funs ;;
| //prints 321
| 4bpp wrote:
| > this issue would have arisen when using capture-by-
| reference
|
| I understand - but in those languages capture-by-
| reference has to be an explicit choice (by writing the &)
| rather than the default, which highlights the actual
| behaviour. The problem with the old Go solution was that
| it would apparently behave as capture by reference
| without any explicit syntactic marker that it is so, and
| without a more natural alternative that captures by
| value, in a context where from other languages you would
| expect that the capture would happen by value.
|
| > Common Lisp and Scheme
|
| I have to admit I haven't worked in either outside of a
| tutorial setting, but my understanding is that they are
| quite well-known for having design choices in variable
| scoping that are unusual and frowned upon in modern
| language design
|
| > Ocaml
|
| Your example shows that it captures by value as I said,
| right? For it to work as the old Go examples, i would
| have to be a ref cell whose contents are updated between
| iterations, which is not how the semantics of for work.
| If it did, you'd have to use the loop counter as !i.
| tsimionescu wrote:
| In Go 1.22 as well, closures still capture-by-reference.
| The change is that there is now a new loop variable in
| each loop iteration, just like in OCaml. But two closures
| that refer the same loop variable (that are created in
| the same iteration, that is) will still see the changes
| each makes to that variable.
|
| And what I was trying to show with my example was that
| this kind of behavior would be observable in OCaml as
| well, if it were to be implemented like that.
| tester756 wrote:
| It is crazy that such behaviour even gets deployed
|
| It is so unintuitive...
| wahern wrote:
| It's unintuitive to users of the language, but it's very
| intuitive from the perspective of those implementing the
| language. Everybody seems to make this mistake. Lua 5.0
| (2003) made this mistake, but they fixed it in Lua 5.1
| (2006). (Lua 5.0 was the first version with full lexical
| scoping.)
| tester756 wrote:
| >It's unintuitive to users of the language, but it's very
| intuitive from the perspective of those implementing the
| language.
|
| It sounds like a lack of dogfooding, lack of review?
| skywhopper wrote:
| The problem is that the error conditions are relatively
| rare. Most of the time it doesn't break anything. So even
| with dogfooding you can miss it or not see it as a
| problem early on. But after 10 years of evidence that it
| was a mistake, that it's almost never intended, and the
| fix won't break much if anything, it's time to fix it.
| masklinn wrote:
| No, it's just an obvious behaviour when you understand
| how the language works.
| tester756 wrote:
| "if you learned how this work, then you understand this
| behaviour"
|
| I disagree with this approach, despite the fact that you
| can logically explain this,
|
| then it still is terrible design and as you see golang
| (and c# and other langs) designers realized it too and
| changed it
|
| So, how can you even try to defend this design when even
| the designers decided to change it (despite the fact that
| changing lang is really hard)?
| masklinn wrote:
| > I disagree with this approach
|
| That is not actually relevant to the point.
|
| > So, how can you even try to defend this design
|
| I'm doing no such thing, I'm pointing out that your
| reasoning is faulty: dogfooding does not help when the
| behaviour is logical and obvious to the designer-cum-
| user, and same with reviewing.
| tester756 wrote:
| >dogfooding does not help when the behaviour is logical
| and obvious to the designer-cum-user, and same with
| reviewing.
|
| While I can agree with dogfodding, then review doesn't
| have to be done just by the people that are responsible
| for the design/impl.
|
| You can have external reviewers for (let's call it)
| sanity check
| catach wrote:
| To the degree the the implementers are also users they
| carry their implementer understanding into their use.
| Dogfooding doesn't help when your understanding doesn't
| match that of your users.
| gtowey wrote:
| It's not crazy. It's just the difference between a pointer
| and a value, which is like comp sci 101.
|
| I think the main things that make it such a trap is that the
| variable type definition is implicit so the fact that it's a
| pointer becomes a bit hidden, and that easy concurrency means
| the value is evaluated outside of the loop execution more
| often.
| Cthulhu_ wrote:
| > It's just the difference between a pointer and a value,
| which is like comp sci 101.
|
| That might be the case, but my comp sci 101 was 15 odd
| years ago now and since then I have _never_ had to think
| about pointers vs values, until I started a Go project a
| few years ago. But even that was more comprehensible than
| the pointer wizardry we had to do in C/C++ back when.
|
| I don't want to have to think about managing my
| application's memory, I much prefer being in the code,
| thinking of variable scope and maintainability which in a
| lot of languages automatically translates to healthy memory
| usage.
| tester756 wrote:
| >It's not crazy.
|
| No. Full disagree.
|
| Array represents a concept of holding multiple values
| (let's simplify) of the same type.
|
| Loop (not index based) over array represents concept of
| going *over* array's elements and executing some code body
| for each of it.
|
| Now, if the behaviour isn't that loop's body is executed
| for each array element (let's forget about returns, breaks,
| etc)
|
| then the design is terrible (or implementation, but that'd
| mean that it was a bug)
|
| I have totally no idea how can you design this thing in
| such a unintuitive way unless by mistake/accidentally.
| jrockway wrote:
| The loop semantics do not have anything to do with
| arrays. The point of confusion is whether a new slot for
| data is being created before each iteration, or whether
| the same slot is being used for each iteration. It turns
| out that the same slot is being used. The Go code itself
| is clear `for i := 0; i < 10; i++`. `i := 0` is where you
| declare i. Nothing else would imply that space is being
| allocated for each iteration; the first clause of the
| (;;) statement is only run before the loop. So you're
| using the same i for every iteration. This is surprising
| despite how explicit it is; programmers expect that a new
| slot is being allocated to store each value of i, so they
| take the address to it, and are surprised when it's at
| the same address every iteration. (Sugar like `for i, x
| := range xs` is even more confusing. The := strongly
| implies creating a new i and x, but it's not doing that!)
|
| Basically, here are two pseudocode implementations. This
| is what currently happens: i =
| malloc(sizeof(int)) *i = 0 loop:
| <code> *i = *i + 1 goto loop if *i < 10
|
| This is what people expect: secret =
| malloc(sizeof(int)) *secret = 0 loop:
| i = malloc(sizeof(int)) *i = *secret
| <code> *secret = *secret + 1 goto loop
| if *secret < 10
|
| You can see that they are not crazy for picking the first
| implementation; it's less instructions and less code, AND
| the for loop is pretty much exactly implementing what
| you're typing in. It's just so easy to forget what you're
| actually saying that most languages are choosing to do
| something like the second example (though no doubt, not
| allocating 8 bytes of memory for each iteration).
|
| Remember, simple cases work: for i :=
| 0; i < 10; i++ { fmt.Println(i) // 0 1 2 3 4
| ... }
|
| It's the tricky cases that are tricky:
| var is []*int for i := 0; i < 10; i++ {
| is = append(is, &i) } for _, i := range
| is { fmt.Println(*i) // 9 9 9 9 9 ...
| }
|
| If you really think about it, the second example is
| exactly what you're asking for. You declared i into
| existence once. Of course its address isn't going to
| change every iteration.
| beeburrt wrote:
| I'm getting 10s for your last code example (not 9s)
| jrockway wrote:
| Ah yup. I didn't test it and you normally never see i
| after that last increment!
| tester756 wrote:
| >The loop semantics do not have anything to do with
| arrays.
|
| Loop in general or "for each" style loop, that's huge
| difference.
|
| The 2nd one has a lot to do with collections.
|
| >You can see that they are not crazy for picking the
| first implementation; it's less instructions and less
| code
|
| Yes, it is not crazy when you're looking at it from the
| reverse engineering / implementation side
|
| but if you start thinking about it from user's
| perspective then it is very bad behaviour
|
| because they used "foreach" like loop which is a concept
| of walking thru every element of collection.
| kweingar wrote:
| I still don't see how looping over a collection is
| different from looping over a sequence of numbers from 1
| to n.
| tester756 wrote:
| Depends what do you actually mean by sequence, but mostly
| purpose.
|
| Normal "for" is like: repeat this code body as long as
| condition is satisfied
|
| Foreach is more like: walk thru this collection
|
| Look (c#):
|
| foreach (var item in items) ...
|
| for (int i=0; i<10; i++) { }
|
| In the 2nd version it is possible to jumps ahead, back,
| do not move, etc. Generally play around "i's" values
|
| Meanwhile I haven't seen yet any1 trying to do anything
| like this in foreach, because it is meant for just
| walking thru collection
| hinkley wrote:
| If I understood the example, Java had this same problem.
| I'm wondering if C# does as well.
| xmcqdpt2 wrote:
| It would be hard to trigger it in Java. All references
| are pass-by-value, so you would have to do something like
| creating an array, passing that array and then replacing
| an element in it in on every loop iteration. Unless I got
| something wrong, it would be hard to do this by mistake
| IMO.
| hinkley wrote:
| If you do an asynchronous callback in an inner loop that
| tries to log the loop counter and a calculated value at
| the same time, you will find that the loop counter has
| incremented underneath you and you'll get for instance
| '20' for all of the logs. That was my introduction to
| this sort of problem.
|
| The solution as I said elsewhere is to pop out the inner
| block to a separate function, where the value of the
| counter is captured when the outer function is called,
| not when the inner one runs.
| simiones wrote:
| I don't think you remember well how you triggered this
| error, since Java just doesn't allow you to reference a
| non-final variable from an inner function. It sounds like
| you're talking about code like this, but this just
| doesn't compile: for (int i = 0; i < n;
| i++) { callbacks.add(new Callback(){
| public void Call() { System.out.println(i);
| //compiler error: local variables referenced from an
| inner class must be final or effectively final
| } }); } for (var f : callbacks) {
| f.Call(); }
|
| Note that code like this works, and does the expected
| thing: for (int i : new int[]{0, 1, 2}) {
| callbacks.add(new Callback(){ public void
| Call() { System.out.println(i); }
| }); } for (var f : callbacks) {
| f.Call(); } //prints 0 1 2
| mik1998 wrote:
| I don't know much about Go but the design seems very
| intuitive to me. You're doing something like (let ((i
| variable)) (loop (setf i ...) ...body)), which if there
| is a closure in the loop body will capture the variable i
| and also subsequent mutations.
|
| The fix is to make a new variable for each iteration,
| which is less obvious implementation wise but as per the
| post works better if you're enclosing over the loop
| variable.
| simiones wrote:
| While I agree that the design is very clear for the cases
| illustrated here, and I am a bit puzzled on why the Go
| designers chose to change these as well, the design is
| not at all clear for the other case: for
| i, v := range []int{1, 2, 3} { funcs =
| append(funcs, func() {fmt.Printf("%v:%v, ", i, v)})
| } for _, fun := range funcs { fun() }
| //prints 2:3, 2:3, 2:3
|
| The reason why this happens is clear. But, it's not what
| people expect from the syntax, not at all. And it's also
| a behavior that is _never_ useful. There is 0 reason to
| capture the loop variables, as evidenced by the fact that
| none of the languages that have started like this and
| taken a breaking change to switch to the expected
| behavior has found even a single bug caused by this
| breaking change.
| mik1998 wrote:
| > And it's also a behavior that is never useful.
|
| False. There are cases where it is useful to have the
| loop variable available directly. For example, you can
| add one to the loop variable to skip an iteration, which
| would not work with an iteration-local loop variable.
| simiones wrote:
| In a for-in-range loop, the variables are read-only
| inside the loop body, so there is no way to skip this.
|
| I do agree that there are reasons to modify the iteration
| variable in a C-style for loop, so I am surprised that
| those loops are being modified as well. C#, which went
| through a similar change, did NOT apply such a change for
| those for loops.
| eru wrote:
| And an optimizing compiler can reduce your latter case to
| the former, if they can prove it's safe to do so.
| baq wrote:
| The problem is when it doesn't matter at first and then your
| language and its use evolves.
| jerf wrote:
| jaredpar on the C# team offered the very first comment on the
| Github issue for this proposal:
| https://github.com/golang/go/discussions/56010
|
| I think it played a large part in helping get past the default-
| deny that any language change proposal should have. The other
| big one for me was the scan done over the open source code base
| and the balance of bugs fixed versus created.
| em-bee wrote:
| as soon as i saw mention of c# going through the same thing,
| i realized that this was discussed before:
| https://news.ycombinator.com/item?id=33160236
| continuitylimit wrote:
| // FIXME: this apparently needs to be "fixed" .. for _, v
| := range intvals { go func(v0 int) {
| fmt.Printf("v:%v v0:%v\n", v, v0) }(v) }
| stefanlindbohm wrote:
| I'm new to Go for a significant project this year and like a lot
| of it, but there are a few things about its philosophy that just
| doesn't click for me and the versioning policy is one of them.
|
| To me, this clearly should be considered a breaking change, which
| I normally would expect to look out for when the major version
| number changes. I get that checking module definitions means
| unchanged code won't break, but it might break code in ways I as
| a programmer would not expect when upgrading minor versions. It
| might be technically correct according to some definition, but
| lacks practicality, which has been a recurring feeling for me as
| I get into the language.
| d3w4s9 wrote:
| Not sure what you mean by "should be considered a breaking
| change", because this _is_ a breaking change, I don 't think
| there is any question about that.
| nzoschke wrote:
| Thank you Go team and project!
|
| Go continues to be my favorite language and experience to build
| and maintain in.
|
| They got so much right from the start, then have managed to make
| consistent well reasoned, meaningful and safe improvements to the
| language over the years.
|
| It's not perfect, nothing is, but the "cost" of maintaining old
| code is so much lower compared to pretty much every other
| language I have used.
| devjab wrote:
| Go is such a productive language to work with, it's absolutely
| mind blowing how little adoption it has around where I live.
| Well I guess Lunar went from node to java to go, and harvested
| insane benefits from it, but a lot of places have issues moving
| into new languages. Not that I think that you should
| necessarily swap to a new hipster tech, I really don't, but Go
| is really the first language we've worked with that competes
| with Python as far as productivity goes. At least in my
| experience.
|
| We'll likely continue using Typescript as our main language for
| a while since we're a small team and it lets us share resources
| better, but we're definitely keeping an eye on Go.
| rcv wrote:
| I typically develop in Python, C++, and Typescript, and
| recently had to implement some code in Go. So far I've found
| it a pretty unpleasant language to use. It feels pedantic
| when I don't need it to be, and yet I have to deal with
| `interface{}` all over the place. Simple things that would be
| a one-liner Python or TS (or even just an std::algorithm and
| a lambda in C++) feel like pulling teeth to me in Go.
|
| I'd love to hear of any resources that can help me understand
| the Zen of Go, because so far I just don't get it.
| vineyardmike wrote:
| One of the big things that I've found helped is to "stop
| being an architect". Basically defer abstraction more.
|
| People, esp from a Java-esque class based world want class
| inheritance and generics and all that jazz. I've found at
| work like 50% of methods and logic that has some sort of
| generic/superclass/OOP style abstraction feature only ever
| has 1 implemented type. Just use that type and when the
| second one shows up... then try to make some sort of
| abstraction.
|
| For context, I can't remember the last time that I actually
| used "interface{}". Actual interfaces are cheap in go, so
| you can define the interface at use-time and pretty cheaply
| add the methods (or a wrapper) if needed.
|
| If you're actually doing abstract algorithms and stuff
| every day at work... you're in the minority so I don't know
| but all the CRUD type services are pretty ergonomic when
| you realize YAGNI when it comes to those extra
| abstractions.
|
| Edit: also f** one liners. Make it 2 or three lines. It's
| ok.
| mseepgood wrote:
| Since the advent of generics I rarely ever use
| `interface{}`.
| badrequest wrote:
| I write Go every day, and can count the number of times per
| year I have to involve an `interface{}` literal on one
| hand. Unless you're doing JSON wrong or working with an API
| that simply doesn't care about returning consistently
| structured data, I can't fathom why you'd be using it "all
| over the place."
| coffeebeqn wrote:
| Me too. We have around a dozen go services and I have
| maybe used or seen interface{} once or twice for a hack.
| Especially after generics. I think the parent comment is
| suffering from poor quality go code. It's like
| complaining about typescript because things in your
| codebase don't have types
| xarope wrote:
| Dealing with databases and data scanning into custom
| structs, you would be writing lots Scanner/Valuer custom
| functions which use interface{}
|
| If you are the lucky ones not dealing with databases, I
| sort-of envy you...!
| kiitos wrote:
| Applications should basically never need to write custom
| Scanner/Valuer functions that deal in interface{}, if you
| find yourself doing that it's a red flag
| Spiwux wrote:
| You discovered the Zen of Go. There are no magic one
| liners. It's boring, explicit and procedural.
|
| Proponents argue that this forced simplicity enhances
| productivity on a larger organisational scale when you take
| things such as onboarding into account.
|
| I'm not sure if that is true. I also think a senior Python
| / Java / etc resource is going to be more productive than a
| senior Go resource.
| goatlover wrote:
| Go seems like the antithesis to Lisp.
| pharmakom wrote:
| ... so the code ends up being really long then.
| Spiwux wrote:
| Yes, pretty much. It's a pain to write, but easy to read.
| On a larger scale the average engineer likely spends more
| time reading code than writing code
| tuetuopay wrote:
| I don't find go that easy to read. It is so verbose that
| the actual business logic ends up buried in a lot of
| boilerplate code. Maybe I'm bad at reading code, but it
| ends up being a lot of text to read for very little
| information.
|
| Like a one-line list comprehension to transform a
| collection is suddenly four lines of go: allocation, loop
| iteration, and append (don't even start me on the append
| function). I don't care about those housekeeping details.
| Let me read the business logic.
| Cthulhu_ wrote:
| It's a tradeoff; I too find one-liner list comprehensions
| like simple transforms or filters easier to read than the
| for loop equivalent.
|
| However, it's a dangerous tool that some people just
| can't be trusted with. Second, if you go full FP style,
| then you can't just hire a Go developer, they need
| additional training to become productive.
|
| Here's an example of functional programming within Go
| taken far: https://github.com/IBM/fp-
| go/blob/main/samples/http/http_tes.... It basically adds
| a DSL on top of Go, which goes against its principles of
| simplicity.
|
| There was another great resource that explains why
| functional programming in Go is a Bad Idea; one is
| function syntax (there's no shorthand (yet?)), the other
| is performance (no tail call optimization), and another
| is Go's formatter will make it very convoluted; I think
| it was this one: https://www.jerf.org/iri/post/2955/
| kiitos wrote:
| Go offers a programming interface at a lower level of
| abstraction than languages like Python or Ruby. What you
| call boilerplate or housekeeping, I consider to be
| mechanical sympathy.
|
| Modulo extremes like Java, the bottleneck for programmers
| understanding code is about semantics, not syntax --
| effectively never the literal SLoC in source files. It's
| not as if for i := range x {
| x[i] = fn(x[i]) }
|
| is any slower to read, or more difficult to parse, or
| whatever, than e.g. x.transform(fn)
|
| in any meaningful sense.
| Cthulhu_ wrote:
| One caveat; if `fn` is declared inline when calling that
| function, it's not very pretty because Go doesn't have a
| function shorthand (yet?):
| x.transform(func(value int) string { return
| fmt.Sprintf("%b", value) })
|
| This quickly becomes more difficult to read, especially
| if you want to chain some operations this way.
|
| But this applies to other languages as well, in JS (which
| has a function shorthand) I prefer to extract the
| predicates and give them a meaningful name.
| pdimitar wrote:
| I don't write much Golang -- mostly using it for my own
| needs because it allows quick iteration but haven't made
| a career out of it -- but for any such cases I just
| extract out the function. I deeply despise such inline
| declarations, they are a sign of somebody trying to be
| too clever and that inevitably ends up inconveniencing
| everyone else, their future selves included.
| tuetuopay wrote:
| You don't need to go the python or ruby route to get such
| benefits. I daily write rust that has a pretty
| comprehensive iterator system, while still getting the
| nitty-gritty in your hands. As some other commenter put
| it, `x.iter().map(function).collect()` is mentally
| translated to "apply function to the collection x" at a
| glance.
|
| between var y []int for _, x :=
| range x { y = append(y, function(x))
| }
|
| and let y =
| x.iter().map(function).collect();
|
| I'll take the second form any day. You express the flow
| of information, and think about transformations to your
| collections.
| saagarjha wrote:
| So my 2C/ as someone who's just been skimming this
| thread: I read the second example faster. I mean it's
| like 2 seconds vs 5 seconds, but in the first I have to
| actually read your loop to see what it's doing, whereas
| in the latter I can just go "oh apply fn over x".
| xmcqdpt2 wrote:
| Your example is very simple though.
|
| What's the go equivalent to
| x.map(fn).filter(predicate)
|
| ie returning a new collection of transformed items which
| is filtered by some predicate? Now we are talking more
| like 5-6 lines of Go.
| kiitos wrote:
| > What's the go equivalent to x.map(fn).filter(predicate)
|
| Probably something like var output []T
| for _, val := range input { if newval :=
| transform(val); allow(newval) { output =
| append(output, newval) } }
|
| No problem?
| pharmakom wrote:
| That is significantly worse than the FP version and I
| think it proves the point.
| kiitos wrote:
| It's fine that you judge it that way, but it's not like
| that judgment is any kind of objective truth. I find it
| superior to the FP version because it is less ambiguous.
| xmcqdpt2 wrote:
| It's definitely less clear though, in that it involves an
| if statement with an assignment, three temporary variable
| declarations, etc. Also, type inference won't detect the
| type of the output automatically from the transform
| function type, and this of course assumes you wanted to
| collect into a slice, but it could be a set, or a list.
|
| For some operations, the Go style of explicit, mostly in-
| place mutations produces more complicated code. Whether
| that's balanced out by the code being "simpler" is not
| clear to me, but I haven't worked with Go.
| kiitos wrote:
| I see it as unambiguously _more_ clear, because it makes
| explicit what the machine will be doing when executing
| the code. Whether map /filter copy values, or mutate in-
| place, or etc. etc. is non-obvious. I mean I'm not saying
| my way is the only way and I appreciate that other people
| have different perspectives but I just want to make it
| clear that "clear" in code isn't an objective measure,
| that's all.
| pharmakom wrote:
| I think that shorter code is easier to read - to a point!
| on balance most code is too long, not too short.
| devjab wrote:
| If I asked you to carve wood, would you prefer a carving
| knife or a Victorinox multipurpose tool? I get that it's a
| bit or a cheesy analogy, but it's basically why I liked Go.
| To me it's the language that Python would have been if
| Python hasn't been designed so long a go and is now caught
| in its myriad of opinions. Because I certainly get why you
| wouldn't like an opinionated language, I really do. It's
| just that after more than a decade, often spent cleaning up
| code for businesses that needed something to work better,
| I've really come to appreciate it when things are very
| simple and maintainable, and Go does that.
|
| Similarly I'm not sure you would like working with
| Typescript in my team. Our linter is extremely pedantic,
| and will sometimes force you to write multiple lines of
| code for what could probably have been a one liner. Not
| always, mind you, but for the things we know will cause
| problems for some new hire down the line. (Or for yourself
| if you're like me and can't remember what you ate for
| breakfast). The smaller the responsibility, the less
| abstraction and the cleaner your code the easier it'll be
| to do something with in 6+ months. Now, our linter is a
| total fascist, but it's a group effort. We each contribute
| and we alter it to make it make sense for us as a team, and
| that's frankly great. It's nice that the ability to do
| this, and the ability to build in-house packages, is so
| easy in the Node ecosystem, but it's still a lot of work
| that Go basically does for you.
|
| So the zen is in relinquishing your freedom to architect
| the "linguistics" of your code and simply work on what
| really matters.
|
| I've never used Interface{}.
| kiitos wrote:
| interface{} is a pretty strong code smell.
| gtowey wrote:
| Go is the language that's not made for you, it's made to
| make the life of the next guy who has to maintain your code
| easier! :-)
| Cthulhu_ wrote:
| That's a great way to phrase it, I'm going to steal that
| :D
| hagbarth wrote:
| One of the reasons I like Go is that it really doesn't try to
| be a hipster language. It's kinda boring, which is great!
| Philip-J-Fry wrote:
| Boring is good when you want to build things that are
| maintainable by 100s of devs.
|
| Something we have experienced over and over is that devs
| moving from languages like C# or Java just love how easy
| and straight forwarding developing in Go is. They pick it
| up in a week or two, the tool chain is just so simple,
| there's no arguing around what languages features we can
| and can't use.
|
| Almost everyone I've spoke to finds it incredibly
| productive. These people want to be delivering features and
| products and it makes it easy for them to do so.
| VirusNewbie wrote:
| Maybe a 100 devs Go is fine, but it gets to be a
| nightmare as you scale beyond that.
|
| Language abstractions exist to prevent having developers
| build their own ad-hoc abstractions, and you find this
| time and time again in languages like Go. You can read
| the Kubernetes code and see what I mean, they go out of
| their way to work around some of the missing language
| features.
| Cthulhu_ wrote:
| Yeah, and that nightmare gets even worse in other
| languages; one motivation for creating Go was the use of
| C/C++ by thousands of developers at Google.
|
| Can you link to some of these workarounds? I'm curious to
| see whether they actually make a lot of difference. In
| theory (and I have no experience with any software
| project with more than ten developers working on it),
| they only made it more difficult by adding cleverness.
| [deleted]
| kaba0 wrote:
| > They got so much right from the start, then have managed to
| make consistent well reasoned, meaningful and safe improvements
| to the language over the years
|
| In which universe? They have to constantly patch the language
| up and go back on previous assumptions.
| christophilus wrote:
| Fast compiler, simple tooling, baked in fmt, simple cross
| platform compilation, decent standard library, a tendency
| towards good enough performance if doing things the Go way,
| async without function coloring. They got some things right
| and some things wrong. When tossing out orthodoxy, you'll
| tend to get some things wrong. I think a lack of sum types is
| my biggest gripe.
| nzoschke wrote:
| The std library is a big part of the magic. It's so
| shocking to go to JS land and see that there are 10
| different 3rd party libraries to make http requests, all
| with wildly different ergonomics all within one code base
| due to cross dependency heck.
|
| In Go there's pretty much only the http package, and any
| 3rd party packages extend it and have the same ergonomics.
|
| For a while my biggest gripe was package management but
| it's a dream where we are now.
| 13415 wrote:
| That's the _first_ language change that can in theory break
| programs (in practice, it won 't). Everything else was just
| additions to the existing language with full backwards
| compatibility. That's the opposite of constantly patching the
| language up.
| kaba0 wrote:
| You can patch things up without breaking backwards
| compatibility.
|
| But, going on a well-trodden path slower than the pioneers
| is not a big achievement.
| badrequest wrote:
| Please point to when Go has broken backwards
| compatibility.
| kaba0 wrote:
| That's not my point.
|
| Smart men learn from the mistakes of others.
| nzoschke wrote:
| Obviously different perspectives in this thread.
|
| Robert Griesemer, Rob Pike, and Ken Thompson are
| objectively smart men and pioneers and have learned from
| lots of mistakes both they and the industry made.
|
| Go embodied a lot of those learnings out of the gate.
|
| If the bar is to be perfect out of the gate that's
| impossible and I can't think of any language that could
| pretend to be so.
|
| Go was very good out of the gate and has slowly but
| surely evolved to be great.
| tonyhb wrote:
| Java only just got green threads (goroutines), 11 years
| after Go 1.0 was introduced. CSP had been around for
| yonks.
|
| Nothing is perfect to begin with, and I think you could
| probably be a bit kinder to Golang here.
| nayuki wrote:
| > Green threads were briefly available in Java between
| 1997 and 2000.
|
| -- https://en.wikipedia.org/wiki/Green_thread
|
| Also see
| https://stackoverflow.com/questions/5713142/green-
| threads-vs... , https://docs.oracle.com/cd/E19455-01/806-
| 3461/6jck06gqe/inde... .
| Cthulhu_ wrote:
| That's the embodiment of Go though; they didn't rush to
| implement generics because they didn't want to repeat
| Java's mistakes (just have a look at http://www.angelikal
| anger.com/GenericsFAQ/FAQSections/TypePa...). They took
| their time with a module / dependency system to avoid the
| mistakes that NodeJS made. Every decision in Go is
| deliberate.
|
| Sure, it may not be perfect, or as elegant as other
| languages, but it's consistent and predictable.
| kaba0 wrote:
| What is java's mistake, a tutorial file?
| vlunkr wrote:
| So how do you think they are "patching things up" more
| than other languages?
| sidewndr46 wrote:
| This nonsense again? Where it is controlled per module,
| effectively making it impossible to review code for correctness
| without checking those controls. This is an anti-feature. If you
| can't be bothered to make a local copy or reference the correct
| variable, the problem is the developer. Not the language.
| RadiozRadioz wrote:
| The problem is the language when its ergonomics coerce most
| developers to assume a construct works in a way it does not.
|
| Programmers are often at fault when a language complex, but I'd
| give them a pass when it's simply counterintuitive.
| cb321 wrote:
| This is indeed an ancient problem. I loved the 1992 Lisp FAQ
| comment by jjwiseman. :-)
|
| Nim handles this with `closureScope` and `capture` ( https://nim-
| lang.org/docs/system.html#closureScope.t,untyped ).
|
| It's debatable if anything non-automatic counts as a "fix".
|
| Nim has for-loop macros that should make it possible to instead
| say `for i in captured(0..5): ...` or maybe `for closed(i) in
| 0..5: ...`. That might be a bit nicer, but would still be non-
| automatic. So, unnecessary qualifications can exist / persist.
|
| Speaking of persistence, sometimes loop bodies evolve and the
| x:=x magic used to be needed but is no longer. Version control
| history might help decide if the extra step was unnecessary or
| vestigial, though it would surely slow things down (and perhaps
| run into intermediate uncompilable states of the code). No idea
| if David Chase and rsc looked at that aspect in their analysis.
| ravivooda wrote:
| Similar: https://github.com/ravivooda/gofor
| chen_dev wrote:
| the 'fix' to the example in the README should be obvious, but
| for reference:
|
| - for _, e := range es {
|
| - pumpUp(&e)
|
| }
|
| + for i := range es {
|
| + pumpUp(&es[i])
|
| }
| timrobinson333 wrote:
| I'll be the first to admit I know almost nothing about go, but
| it's surprises me to find we're still inventing languages with
| bobby traps like this, especially bobby traps that were well
| known and understood in other languages at the time.
|
| Actually it surprises me we're still inventing languages where
| local variables can be mutated, which seems to be at the root of
| the problem here
| wrboyce wrote:
| Completely unrelated to the point you're making, but the phrase
| is "booby trap"; I believe it originates from pranks played on
| younger schoolboys in 1600s England (the etymology of booby
| being the Spanish "bobo").
| mixmastamyk wrote:
| > where local variables can be mutated
|
| They aren't local, but belong to the outer scope. The
| misconception in a nutshell.
| tazjin wrote:
| Go has a long list of booby traps like this and prides itself
| on them. From outside of the Go team it looks like a small
| cultural shift might slowly be happening, cleaning up some of
| the obvious mistakes everyone's been telling them about since
| the beginning. Rob Pike retiring and giving up some formal
| power with that probably helps.
| thiht wrote:
| > Go has a long list of booby traps like this
|
| Huh? Where's the list? From the top of my head I think this
| is the only thing that repeatedly bit me, although I'm very
| aware of the behavior of for loop scoping. Linters save me
| nowadays at least.
|
| Are there other things like that in the language that deserve
| a fix? Maybe things to do with json un/marshaling?
| telotortium wrote:
| Typed versus untyped nil (not sure how this could be easily
| fixed though)
| the_gipsy wrote:
| My number one is not having algebraic/sum/union types
| leading to needing zero-values. Which is more like a never
| idling foot gatling gun.
| [deleted]
| kevincox wrote:
| That the printf functions shit garbage into your output if
| you get the formatting specifiers wrong.
|
| I'd much rather have a crash than silently corrupting
| output.
| nu11ptr wrote:
| It has been a while, but yes, there were a lot of them and
| I forget most. It made it kinda pointless to me that the
| language was "easy" when the code felt so brittle (Null
| pointers...really?).
|
| One weird thing that always goofed me up was that slices
| are passed by value but maps by reference. Always made it
| confusing how to pass them for
| serialization/deserialization. The compiler didn't complain
| it just panicked. Seemed like something the type system
| should catch.
| skitter wrote:
| If you append to a slice, you can't rely on the changes
| showing up in the original slice, nor on them not showing
| up.
| wazzaps wrote:
| Copying a mutex by value (thus duplicating the lock,
| causing deadlocks or worse) is far too easy
| icholy wrote:
| `go vet` catches this.
| erik_seaberg wrote:
| Is there a way to declare any type uncopyable? This is
| something I always thought Ada got right.
| [deleted]
| smasher164 wrote:
| you stick this in your struct type
| noCopy struct{} func (*noCopy) Lock() {}
| func (*noCopy) Unlock() {}
| mhh__ wrote:
| I use a tool called "type theory"
| icholy wrote:
| Keep posting about D, I'm sure it will catch on soon.
| usefulcat wrote:
| Wow, even c++ won't let you do that (without invoking
| undefined behavior, anyway).
| rsc wrote:
| Speaking as the person Rob Pike handed the formal power to (8
| years ago now), I don't think that change has much to do with
| it.
|
| We've known about the problem for a long time. I have notes
| from the run up to Go 1 (circa 2011) where we considered
| making this change, but it didn't seem like a huge problem,
| and we were concerned about breaking old code, so on balance
| it didn't seem worth it.
|
| Two things moved the needle on this particular change:
|
| 1. A few years ago David Chase took the time to make the
| change in the compiler and inventory what it broke in a large
| code base (Google's, but any code base would have worked for
| that purpose). Seeing that real-world data made it clear that
| the problem was more serious than we realized and needed to
| be addressed. That is, it made clear that the positive side
| of the balance was heavier than we thought it was back in
| 2011.
|
| 2. The design of Go modules added a go version line, which we
| can key the change off. That completely avoids breaking any
| old code. That zeroed out the negative side of the balance.
| orblivion wrote:
| What about let's say 5 years from now, someone digs up a Go
| project from 2022, decides to get it up to speed for 2028,
| updates the version line. Is there something that would
| remind them to check for breaking changes, including this
| one? Perhaps the go project initializer could add a comment
| above the version line with a URL with a list of such
| changes. Though, that wouldn't help for this change.
| [deleted]
| flakes wrote:
| I think the key difference here is to consider toleration
| vs adoption. Old code is able to tolerate the changes and
| still work in new ecosystems. There is still work on
| maintainers if they want to actually adopt the features
| themselves. Allowing these two concepts to work together
| is what allows iteratively updating the world, rather
| than requiring big bang introduction of features.
|
| As for validating your software, the answer is the same
| as its always been... tests, tests and more tests.
| smasher164 wrote:
| > we're still inventing languages where local variables can be
| mutated
|
| Local mutability is probably one of the most common uses of
| mutability. A lot of it is using local state to build up a more
| complicated structure, and then getting rid of that state.
| Getting rid of that use-case is just giving up performance.
| [deleted]
| zdimension wrote:
| There is a recurring joke about Go's language design ignoring
| many bits of the general language design knowledge collectively
| acquired through decades of writing new languages. This change
| is an example of why this joke exists.
| kzrdude wrote:
| I have run into this problem in Python too, but not recently. I'm
| not sure if Python has changed or if I just caught on to the
| problem.
|
| This should be enough to show that it still can wind up as a
| problem in Python: funcs = [(lambda: x) for x
| in range(3)] funcs[0]() # outputs: 2
| pulvinar wrote:
| GPT-4 says: The behavior you're observing is due to the late
| binding nature of closures in Python. When you use a lambda
| inside a list comprehension (or any loop), it captures a
| reference to the variable x, not its current value. By the time
| you call funcs[0](), x has already been set to the last value
| in the range, which is 2.
|
| To get the desired behavior, you can pass x as a default
| argument to the lambda: funcs = [(lambda x=x:
| x) for x in range(3)] funcs[0]() # outputs 0
| paulddraper wrote:
| That is correct.
|
| Python used to be worse; it used to share scope _outside the
| list comprehension_.
| nightfly wrote:
| It still does for regular loops right?
| thatguysaguy wrote:
| Yeah loops don't get their own scopes (unless you add one
| using this thing I made as a joke:
| https://github.com/davisyoshida/360blockscope)
| DonnyV wrote:
| GO syntax is so hard to look at.
| knodi wrote:
| If you hate readability, sure.
| guessmyname wrote:
| > _GO syntax is so hard to look at._
|
| What do you mean by "hard"?
|
| I find Rust syntax challenging to grasp in a specific way. Rust
| employs numerous symbols and expressions to convey statements,
| which makes reading Rust code a process of constantly
| navigating between different keywords, left and right. I have
| to create a mental map of what certain statements are
| accomplishing before I can truly comprehend the code.
|
| In contrast, I find Go code relatively straightforward,
| especially for those familiar with C-like programming
| languages. This clarity is due to the deliberate verbosity of
| the language, which I personally appreciate, as well as the use
| of early return statements.
|
| But don't get me wrong. I enjoy programming in both Rust and Go
| when they are suitable for the task at hand, but I usually
| spend more time grappling with Rust's syntax than with Go's,
| because I often invest more time in understanding the structure
| and logic of Rust programs compared to their Go counterparts.
| tuetuopay wrote:
| I guess it depends on the way the brain works. I have very
| bad memory, but I prefer the expressiveness of Rust to the
| verbosity of Go. I value much more having the whole context
| on the screen that navigating countless words of boilerplate
| code. I do agree that it gets a bit of getting used to, but I
| find it easier to recognize by eye.
| ShamelessC wrote:
| At least part of the issue for me was that many
| keywords/syntax rules don't match anything I'm familiar with,
| even considering "C-like" languages.
|
| I have similar issues with Rust actually. There's a lot of
| sugar used that you have to grok and that takes some time.
|
| On the other hand Python, C#, Java all stick with a set of
| fairly familiar conventions. In terms of syntax (and only
| syntax), the learning curve is more intense with Go; perhaps
| similar to the initial alienation provided by JavaScript.
|
| My experience has been that once you are being paid to learn
| a language these problems mostly disappear. Alas, no one ever
| paid me to learn Go.
| campbel wrote:
| Won't this end up breaking programs that depend on the current
| behavior?
| [deleted]
| minroot wrote:
| Some time spent with Go gives a strong indication that Go team
| always has backwards compatibility in mind
| jacquesm wrote:
| They do. Go has avoided most of the pitfalls that other
| language eco-systems have fallen for over the years
| (backwards compatibility issues, soft forks masquerading as
| language improvements, re-booting the whole language under
| the same name, aggressively pushing down on other languages
| etc). They've done _remarkably_ well in those respects, and
| should deserve huge credit for it.
| nerdponx wrote:
| Python has the same problem (to the extent that it's actually a
| problem, which you might or might not agree with), and this is
| the #1 reason they won't change it.
| omeid2 wrote:
| I don't know why you're being down voted, but it is actually
| breaking the Go1 compat promise. Which says:
| It is intended that programs written to the Go 1 specification
| will continue to compile and run correctly, unchanged, over the
| lifetime of that specification. At some indefinite point, a Go
| 2 specification may arise, but until that time, Go programs
| that work today should continue to work even as future "point"
| releases of Go 1 arise (Go 1.1, Go 1.2, etc.).
| jacquesm wrote:
| And they do. You can specify the precise logic to use on a
| per-file basis.
| Cthulhu_ wrote:
| No, it doesn't break the promise; "Go programs that work
| today should continue to work even as future "point" releases
| of Go 1 arise (Go 1.1, Go 1.2, etc.)."
|
| You can install Go 1.22 and your program will compile and run
| as-is. That's the promise. If however you opt-in to the
| changed for loop behaviour by adjusting your go.mod, the onus
| is on you to update your program accordingly.
|
| It's only a backwards incompatible change if the developer
| makes a backwards incompatible change by updating the
| configured target version.
|
| (I'm aware I'm probably being pedantic here, I understand the
| language used seems to imply you can just set it to v1.22 and
| it works but it's a bit more specific)
| efuquen wrote:
| In a previous blog post they basically said they will never
| make a Go 2, and also addressed a lot of things about
| compatibility:
|
| https://go.dev/blog/compat
|
| In particular they said:
|
| > The end of the document warns, "[It] is impossible to
| guarantee that no future change will break any program." Then
| it lays out a number of reasons why programs might still
| break.
|
| > For example, it makes sense that if your program depends on
| a buggy behavior and we fix the bug, your program will break.
| But we try very hard to break as little as possible and keep
| Go boring.
| Cthulhu_ wrote:
| > In a previous blog post they basically said they will
| never make a Go 2
|
| No, they didn't say that, they said it wouldn't be
| backwards-incompatible with Go 1. Relevant quote:
|
| > [...] when should we expect the Go 2 specification that
| breaks old Go 1 programs?
|
| > The answer is never. Go 2, _in the sense of breaking with
| the past and no longer compiling old programs_ , is never
| going to happen. Go 2 in the sense of being the major
| revision of Go 1 we started toward in 2017 has already
| happened.
| wrs wrote:
| Note the word "programs", not "files". If your program
| doesn't declare go 1.22 in its go.mod, it will continue to
| work (or not work!), unchanged.
| robertlagrant wrote:
| Isn't that "working with future point releases", though? If
| I don't declare 1.22, am I not excluded from that point
| release?
| ben0x539 wrote:
| I assume that if compiling with 1.22 or later, you still
| get all the benefits from that version like other new
| features, bug fixes or perf improvements, just not this
| particular change.
| mrkstu wrote:
| No, the compiler will revert to the original behavior, it
| only adopts the new behavior with the declaration.
| skywhopper wrote:
| This has all been addressed in the proposal. The research
| was done and this change will impact so few projects that
| it's worth making a technical exception to the
| compatibility promise to fix a real design flaw.
| freedomben wrote:
| I upvoted the question to offset one of the downs because I
| agree it's a fair question. However I would guess the
| downvotes are because TFA addressed this issue directly and
| comprehensively, so it's a clear "I didn't read the article"
| indicator :-) Possibly also because the downvoters can't
| imagine a scenario where this would be desirable behavior
| (i.e. it's always a bug)
| colejohnson66 wrote:
| But if it's a bug, then the logic to not compile future
| versions is wrong, IMO. If it's a feature change, then such
| logic would make sense.
| campbel wrote:
| Yeah its fair, I didn't closely read that section.
| Although, I'm not entirely convinced the approach is safe,
| maybe its worth it to fix such a common pitfall.
| [deleted]
| campbel wrote:
| I don't mind.
|
| Yeah, I thought this kind of change wouldn't happen because
| of this promise.
| tgv wrote:
| I think it was downvoted precisely because of that. It's a
| bit of a contentious issue.
| skywhopper wrote:
| The original proposal for this change went into great detail
| about the research they did into existing uses of this syntax.
| In my memory, they found vanishingly few cases in the Google
| codebase or GitHub code where the change would violate the
| expected behavior. The decision to break the backwards
| compatibility here came only after determining how few
| codebases would be affected and developing a mechanism in Go
| itself (the version specification in go.mod) to require
| actively modifying the code to build with the new behavior.
| doctor_eval wrote:
| To add to the other comments, in the run-up to go1.21 they
| talked about how they'd analysed a very large corpus of Go code
| to see what would be affected, and it was a very very small
| number.
|
| I remember thinking that the number of people who have created
| inadvertent bugs due to this design (myself included) would be
| significantly greater than the number of people affected by the
| fix.
| beltsazar wrote:
| Yes, the loopvar change will break some programs, and hence the
| compatibility promise. But the Go team argues that the change
| will fix much more programs than it will break [1].
|
| This makes me wonder, though, what guarantees that a similar
| breaking change won't ever happen again in the future? If any
| change with #(programs fixed) >> #(programs broken) is
| accepted, we might as well remove the compatibility promise
| page [2].
|
| ---
|
| [1] https://github.com/golang/go/issues/60078
|
| [2] https://go.dev/doc/go1compat
| omginternets wrote:
| Seems like yes, though hopefully that should be rare.
| baq wrote:
| You may also wonder how many would it fix if it wasn't hidden
| behind a flag?
| tapirl wrote:
| A lots:
|
| https://twitter.com/go100and1/status/1690412229135601664
|
| https://twitter.com/go100and1/status/1690587305806057472
|
| https://twitter.com/go100and1/status/1690589791686119424
|
| https://twitter.com/go100and1/status/1690591234715492352
|
| https://twitter.com/go100and1/status/1690593184857145344
|
| https://twitter.com/go100and1/status/1691456732151889920
|
| Most of them are not mentioned in the proposal doc at all.
| matthewmueller wrote:
| It's only enabled for modules that run Go 1.22 and higher
| badrequest wrote:
| I also can't imagine a case where it is useful or even truly
| intended to rely on this behavior.
| ben0x539 wrote:
| Yeah I don't think it's so much "we explicitly rely on this
| behavior, how dare you change this" as "somewhere in our
| mountains of maintenance-mode code that haven't seen the
| sun shine through an editor window in years, this behavior
| cancels out another bug that we never noticed". Tooling
| should be able to detect when code relies on this, but it's
| still gonna cost some non-zero amount of developer effort
| to touch ancient code and safely roll out a new version if
| it needs to be actively addressed.
| rsc wrote:
| If you have tests and they break with
| GOEXPERIMENT=loopvar, then there is a new tool that will
| tell you exactly which loop is causing the breakage.
| That's a post for a few weeks from now.
| kuchenbecker wrote:
| If
| Cthulhu_ wrote:
| Neither can I, but there may be cases of code accidentally
| relying on it - there's an adage that I forgot the name of
| that says just that, and I think compiler manufacturers are
| the most aware of that adage.
| omginternets wrote:
| Yeah it's def a code smell ...
| infogulch wrote:
| > To ensure backwards compatibility with existing code, the new
| semantics will only apply in packages contained in modules that
| declare go 1.22 or later in their go.mod files. ... It is also
| possible to use //go:build lines to control the decision on a
| per-file basis.
| sixstringtheory wrote:
| Doesn't that mean that all code written so far can't take up
| newer versions of the Go compiler for any other reason like
| new features/bugfixes/optimizations/etc without a full audit
| of codepaths involving for loops?
| ericpauley wrote:
| No, it does not. Packages can compile using 1.22 and gain
| other benefits without opting into this change.
| sixstringtheory wrote:
| Ah, I didn't see the part about //go:build
| acheong08 wrote:
| Without /:go:build tags, you can just define 1.21 as your
| Go version in go.mod to opt out of new features while
| getting other benefits of the new compiler
| Cthulhu_ wrote:
| No I don't think so; any old working code will be using the
| x := x workaround, which will keep working when going to
| this version with the changed loop mechanics. What may
| happen is a form of... some adage, I forgot the name, where
| code accidentally relies on the old behaviour and breaks
| when that old behaviour is no longer there.
|
| (that same adage applies to e.g. browser manufacturers
| having to implement bugs to not break certain websites)
| Quekid5 wrote:
| Are you thinking of Hyrum's Law?
| kevincox wrote:
| No, the version declared in go.mod is different than the
| version of the toolchain used to compile the project. If
| you declare an older version even new toolchains will act
| like the previous versions.
| s17n wrote:
| There isn't really anything that you could actually use the
| old behavior for, any code that depends on it is probably
| wrong anyway.
| matthewmueller wrote:
| Anyone know how this will impact loop performance?
| tedunangst wrote:
| Nonexistent? It can still reuse the memory if you don't capture
| it. And if you were capturing it "properly" it was already
| making a copy.
| jsmith45 wrote:
| Most commonly no impact. It can require an additional heap
| allocation per iteration if taking the address or capturing in
| a closure, but even in those cases escape analysis may be able
| to determine that the value can remain on the stack because it
| will not remain referenced longer than the current loop
| iteration. If that happens then this change has no impact.
|
| I'm not sure how thorough Go's escape analysis is, but nearly
| all programs that capture the loop variable in a closure and
| are not buggy right now could be shown to have that closure not
| escape by a sufficient thorough escape analysis. On the other
| hand for existing buggy programs, then perf hit is the same as
| assigning a variable and capturing that (the normal fix for the
| bug).
|
| Google saw no statistically significant change in their
| benchmarks or internal applications.
|
| https://github.com/golang/go/wiki/LoopvarExperiment#will-the...
| mongol wrote:
| Is this a common way to fix problems in language syntax? It seems
| unintuitive to me. Now you need to know what version is declared
| in one file to understand behavior in another file. I understand
| they want to fix this but I did not know this way was allowed.
| kevincox wrote:
| I don't think it is common. But probably the best option in
| this case.
|
| The next best alternative is introducing a new construct for
| this version. But then you either risk people still using the
| old one or you need to break lots of fine code by removing the
| old construct. So in this case the "in place" upgrade made the
| most sense.
|
| Tying it to the declared compiler version is much like Rust's
| edition system or Perl's versioning, except tacked into an
| existing identifier rather than a separate variable. (The
| downside being that you are forced to make this upgrade at some
| point if you want to raise your minimum toolchains version. )
| jjwiseman wrote:
| I know there are much earlier examples, but the earliest warning
| about this behavior I could find in 60 seconds of searching is
| from the comp.lang.lisp FAQ, posted more than 30 years ago, in
| 1992: Mar 21, 1992, 1:00:47 AM Last-
| Modified: Tue Feb 25 17:34:30 1992 by Mark Kantrowitz ;;;
| ****************************************************************
| ;;; Answers to Frequently Asked Questions about Lisp
| *************** ;;;
| ****************************************************************
| ;;; Written by Mark Kantrowitz and Barry Margolin ;;;
| lisp-faq-3.text -- 16886 bytes [...]
| ----------------------------------------------------------------
| [3-9] Closures don't seem to work properly when referring to the
| iteration variable in DOLIST, DOTIMES and DO.
| DOTIMES, DOLIST, and DO all use assignment instead of binding to
| update the value of the iteration variables. So something like
| (dotimes (n 10) (push #'(lambda () (incf n))
| *counters*)) will produce 10 closures over the
| same value of the variable N.
| ----------------------------------------------------------------
| [deleted]
| sixthDot wrote:
| D too https://issues.dlang.org/show_bug.cgi?id=2043.
|
| That's actually expected when capturing by reference.
| junke wrote:
| In the standard it is not specified if such loops mutate or
| rebind, and you have to assume it doesn't rebind if you capture
| variables. I do think however that once you learn how it works
| it stops being a problem (in any case I can select the form,
| macroexpand it and it shows how it's implemented)
| varjag wrote:
| In theory sure. In practice it's easy enough to make this
| mistake mindlessly. I had this happen to me after many years
| of practice just this year (in an elaborate extended LOOP
| form which has same semantics).
___________________________________________________________________
(page generated 2023-09-20 23:02 UTC)