Go and Versioning: Minimal Version Selection
research.swtch.com
research.swtch.com
To that end, I even created Go Package Store [0] to make updating packages in one's GOPATH easier and more transparent. I use it at least once every few days to update my dependencies and deal with anything that comes up (rarely; most often I just need to leave a review comment on the fresh CL/PR). It's a great experience for me.
Despite that, I have a feeling I might like MVS better than the traditional manifest+lock systems that try to pick the latest versions at project start time, but then keep them frozen there until people explicitly update. It makes people less likely to update on their own because they know a new project would start off with latest versions of dependencies anyway.
With this system, you couldn't rely on that, and instead you have to update yourself explicitly from the start. I hope this knowledge gets people to take on that responsibility more often and makes it a good habit. Don't update when you need build stability, but when you have a quiet period and are able to update for some API changes, do so and reduce your tech debt for the future.
> More than anything else, I wanted to find a version selection algorithm that was understandable. Predictable. Boring. Where other systems instead seem to optimize for displays of raw flexibility and power, minimal version selection aims to be invisible. I hope it succeeds.
I'm keeping an open mind and looking forward to seeing how it works out. I have a feeling I might like it despite it seemingly optimizing for the opposite of what I fight for. I'm hopeful.
It becomes a problem when a full rebuild includes deleting external dependencies, downloading latest sources, and rebuilding everything. When that happens your build environment is a constant moving target. The problem grows exponentially with the number of dependencies you have. When you have a project with a 1000+ dependencies, all pulled latest, nothing will ever compile, let alone your own code.
The default should be to update constantly. Expecting developers to remember to manually trigger updates just results in perpetually-stale dependencies. If a specific issue is found with a newer version, then having the ability to explicitly pin to an old version is reasonable.
> When you have a project with a 1000+ dependencies, all pulled latest, nothing will ever compile, let alone your own code.
The problem is having a project with 1,000+ dependencies that are apparently such low quality that their authors issue build-breaking changes daily.
I'd argue dependencies should be manually updated constantly.
While version pinning does allow you to accrue technical debt if your team has discipline it does allow you to manage the upgrade process and understand the impacts of a dependency upgrade.
Even without obvious API changes it's possible that the implementation details of a library's method can cause your code to function differently, which can be caused by the introduction of a bug, the fixing of a bug your code didn't realize was a bug, or any minor quirks that may change between versions. When you manually upgrade you know exactly what to test and what to look for because (presumably) the person doing the dependency upgrade is someone who knows how the dependency is used. If you do not do manual upgrades but automatically update everything all the time bugs can sneak in and it won't always be easy to track down what caused it.
The Node concept of "package" management is really so different from traditional systems that they really shouldnt be discussed in the same context or as having the same concerns.
I also have philosophical concerns about selecting the minimum version: people release patch versions for a reason. I hope vgo only uses the minimum for minor versions not patches (even then I still prefer the newest minor — most of the fixes I release are not back ported as patches). However, I don’t think this algorithm is a bad choice and it seems to fit the go philosophy nicely as it’s simple and to make it “better” gets into many thornier issues that may even involve language changes ala project jigsaw for Java.
This should probably be addressed explicitly, but it's only been what, 1 day, since vgo was announced?
Though part-4 hasn't been added to it just yet.
This is the next post in the series discussed here: https://news.ycombinator.com/item?id=16421966
I thought we cannot have cycles in Go packages. The Go spec says "The importing of packages, by construction, guarantees that there can be no cyclic initialization dependencies." What am I missing?
What would be nice would be a command to upgrade to latest of the direct dependencies - and then transitively pull in the latest version of packages they have been tested with.
vgo get -u
(As a reminiscence to go get -u) should do what you want.
I might missinterpret your need though.
edit : I can of course do it one by one explicitly for B and C. But it would be nice to qualify e.g. : vgo get -u -directonly
Not sure if i did it right, since the default template seems to be for a bug and not a new feature request.
In other words, gofmt showed that it's easy to write a pretty printer if you ignore line breaking and instead force everyone to accept long lines. This is not a particularly interesting, and certainly not revolutionary, result.
(Not coincidentally, I have similar concerns regarding this Go versioning proposal and lockfiles.)
You don't need to accept long lines as gofmt respects your own line wrapping. It's easy to write code that never exceeds the 80 character limit. The main annoyance in go is it's use of tabs, so you have to change tabs in everything to a reasonable amount of space.
As such, I think your complaints are misplaced. gofmt is not a pretty printer. It's designed to implement a single standard format—one which does not define a maximum line width—but nothing more.
Indent, by the way, allows for a maximum line length.
It may help if you decouple the goal (increased readability of code) and the action (application of a style to code). Pretty printers—and gofmt—apply a style to code. That is their only job; in a traditional pretty printer, you could configure a terrifyingly awful style of code, and it would apply it happily and correctly. The readability of that style is a subjective judgment to others, but this is in no way a concern of the pretty printer.
You may not like the style of code that Go uses. That is a perfectly valid viewpoint, and I encourage you to develop it as you see fit! It is also perfectly valid to criticize the style choice of having an undefined maximum line width.
The problem gofmt solves is not "how do we allow you to style Go code", but more specifically, the problem it solves is "how do we encourage the community to adhere to one style of code for Go?" And it is successful in that. It is obviously unsuccessful in applying a style that you find aesthetically pleasing—alas!
Also, I am afraid that I did not claim indent does not allow for a maximum line width! I said that indent (like other pretty printers) allowed you to define a style "with a specific indent level" and that "you could mix in your own preference for indent level".
It's not successful if everyone puts line breaks in different places. By punting on that problem, gofmt isn't enforcing one style of code.
A typical pretty printer has knobs and dials to control its output, for instance — gofmt has none.
gofmt was a revolutionary social achievement: it got a huge number of people to basically all format their code the same. That had never been achieved before; prior to Go doing it, I would not have even thought it would be possible.
(Obviously, not revolutionary in any sort of technical sense.)
Having the editor wrap lines is contrary to standard practice in pretty much every other language. It requires extra editor configuration for no real benefit other than making gofmt a shorter program. Besides, editors don't wrap lines as nicely as a good pretty printer can. Again, look at clang-format: the heuristics it applies in order to apply a maximum line width are very carefully thought out.
Though, Python does do a better job than many languages of providing style guidance (pep 8 iirc), and I don't really see people do wacky stuff like messing with where to put space around parens. &c.
Can't speak to Fortran.
Thinking about it, maybe languages like J or APL are always consistently formatted by being so dense there's no room for extraneous formatting?
But certainly for C-like languages I've used, just getting people on the same team to all use the same format was a struggle, much less the whole company, much much less the whole world.
So in that way, gofmt definitely feels revolutionary to me.
clang-format has really changed the game here. It's commonplace in large projects to run it on every commit.
There is a difference between C and C++ formatting and Go formatting, and it's that Go had gofmt from the start and has always had large social pressure to conform to it. Contrary to popular sentiment, I'm not sure that this is an unmitigated good, though. The reason is that clang-format was designed to mimic the existing style that emerges when programmers manually format code for maximum readability. This forced clang-format to consider comment formatting, line length, argument bin packing, etc. It had to be good, or else it wouldn't get use. On the other hand, because gofmt was guaranteed to get lots of use no matter what, there was less pressure for the result to be readable. This shows up in decisions like the lack of a line length limit.
Well, again, I personally don't think line limit should be part of the code style -- it's really handy to be able to split or not split emacs buffers and have the code spill out to take up the space, wrapping dynamically. If the code hard-wraps, you're just stuck w/ that single decision. It's annoying.
Regarding Go's style more generally: Yeah, my own personal pref would be much denser that gofmt, but I happily submit for the consistency.
I just don't think style really matters that much (within the realm of reasonable, which gofmt certainly is): the important thing is consistency.
(By the way, a nice side-effect of consistency: you can easily grep for a particular thing w/ confidence w/o having to try to take into account all the possible stylistic variations.)
We were using indent with CVS pre-commit hooks in 1999, hardly revolutionary.
but yeah, team style was definitely a solvable problem. global style, manifestly was not solved!
I also just happen to really like things neat a tidy.
Final minor nice thing about having a fixed style: you can grep with confidence, without having to try to encode stylistic variations in the regex.
gofmt imposes a single style on the entire go ecosystem, that's unusual as most previous formatters have lots of options, and allow different teams to adopt different styles (e.g. tabs vs spaces). Turns out it doesn't really matter what style you choose as long as everyone uses the same one, and long lines aren't a problem in practice.
Worse is better.
It's just a philosophical stance, and a rather totalitarian one IMHO.
I disagree on that - it makes it much easier to share code and to use code from other teams, and removes a whole area which people waste a lot of time on (bikeshedding formatting issues).
(Again, maybe line length doesn't matter much in Go, since lines generally don't get that long in Go, but for lots of other languages gofmt's choice would not be acceptable.)
Interestingly enough, I've never seen people bikeshed about code formating in a project using a style formater even if it is configurable. But everytime Go is talked about here on HN, there are many people complaining about gofmt not being configurable …
Let's say I have this program (which was goformatted):
package main
import (
"fmt"
)
// commenting...
// commenting...
func main() {
fmt.Println("hello", "world")
fmt.Println(
"hello",
"world",
)
}
And I had log.Print("test") at the end of main(): package main
import (
"fmt"
)
// commenting...
// commenting...
func main() {
fmt.Println("hello", "world")
fmt.Println(
"hello",
"world",
)
log.Print("test")
}
Then I run goimports to automatically update the import directive (goimports parses the source to build an AST, then modifies the AST, then reconverts it to source code): package main
import (
"fmt"
"log"
)
// commenting...
// commenting...
func main() {
fmt.Println("hello", "world")
fmt.Println(
"hello",
"world",
)
log.Print("test")
}
Everything is kept exactly as-is, except the added line "log" in the import directive. This round-trip mechanism is the main benefit of gofmt.It's also likely, in practical terms, to feel very different to work with than any existing packaging system.
My money is on it becoming quite popular.
It just feels like it's been sprinkled w/ the Go magic -- deceptively simple ideas that end up working really well.
Yes, Rust's Cargo: see https://github.com/rust-lang/cargo/blob/0.25.0/src/cargo/cor... For demonstration, on my low-end VPS Cargo takes less than a quarter second to select the versions of the 289 transitive dependencies used by Servo (could be much less, but I don't have a precise measurement; `time cargo update` takes 1.22 seconds but this is dominated by the network calls checking for a new version of the crates.io index and checking for new versions of Servo's custom deps on Github).
The fact that the algorithm in the OP isn't NP-hard isn't anything worth getting excited about. The only advantage I can see is that it gives a measure of reproducibility without the need for lockfiles--though builds will still be less reproducible than they'd be with lockfiles.
> Yes, Rust's Cargo: https://github.com/rust-lang/cargo/blob/master/src/cargo/cor.... For comparison, on my low-end VPS Cargo takes less than a quarter second to select the versions of the 289 transitive dependencies used by Servo
That's kinda an answer to a different question. The question you replied to was asking about other non-NP-hard systems and you replied with how much time your system took.
https://github.com/rust-lang/cargo/blob/0.25.0/src/cargo/cor...
> Actually solving a constraint graph is an NP-hard problem. This algorithm is basically a nice heuristic to make sure we get roughly the best answer most of the time.
That's miles away from the simple, linear time algorithm proposed here.
Maybe it doesn't matter (though I suspect we'll discover it does), but that's not what you were arguing.
Besides, I suspect that if you restrict your Cargo graphs to use only the features that this Go proposal supports (minimal version pinning, in particular), then the algorithm is no longer NP-hard. There's no "Go magic" here: it's that just Go chooses not to implement features that could theoretically be slow (but aren't slow in practice).
Might well be totally irrelevant here.
Sadly I don't know Rust (yet!), but Cargo certainly seems widely loved and admired.
Perusing the resolver code was pleasant, and impressive. We've come a long way from when you'd pull back the covers on a C++ std library or (god help you) boost, and run away screaming in terror.
The future is bright!
Again, these things aren’t novel; in isolation, they’ve been considered (and rejected) by other package managers. Maybe they compose to something greater than the sum of their parts. At the moment, it’s unclear, and it mostly looks messy.
Is there something we can do about forcing a check to ensure things aren't out of sync? Maybe a module can define dependencies as internet/external? If a dependency is marked as external, the referencing library must reference that same exact dependency/library version?
Not that I'd want to, just wondering how it works.
It works if they don't reference any global resources outside of their own package. If they do reference any global resources (such as the using the standard flag package, paths on the filesystem, mutexes etc.), then things can fail badly. Yes, this means if one of your dependencies pulls in a different major version of a package than used by other dependencies, then they can end up duelling over resources.