One of my biggest fights is getting people to use latest versions of high-quality software. More often than not, it avoids problems that are already resolved at tip. Helps discover newly introduced issues quickly and before it's a lot of trouble to fix them. Overall, I think it amortizes the cost of software development and reduces it on average, even if there are more smaller bursts where fixing work needs to be done, it's often trivial to do so.
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's OK as long as you are in charge of when the updates happen. Your system is good in that it is a manually triggered process.
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.
> When you have a project with a 1000+ dependencies
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.
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.
> 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.
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.
in my experience traceability of the versions of the dependencies used is as important as tracing that you use them... having that all in source control and being to replicate an immutable build from that tag at any time in the future is useful especially for large and complicated projects.
Perpetually-stale is just a pejorative way of saying known-good. You should update frequently, but only when you have time to test for problems and when it's not an especially bad day to cause an outage for your clients.
I still remain unconvinced at the lack of lockfiles. I’m in complete agreement with rsc the minimum version algorithm produces more reliable builds, ie version selection is deterministic. However, lockfiles do more than select versions deterministically, they also hash the packages so that you can be guaranteed that version 1.1 today is the same as 1.1 tomorrow.
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.
I don't see why hashing is incompatible with the minimum version algorithm. As far as I can tell, the algorithm will always pick versions that are specified in one module file unless a version that doesn't exist was added by hand. If the algorithm always selects a version specified in modules, then that's a perfectly good place to put hash values.
This should probably be addressed explicitly, but it's only been what, 1 day, since vgo was announced?
It’s not. But it’s not in the current tool. And as this article devoted a long section to why they’re not required for reproducibility I suspect the go team isn’t planning on added one. It’s especially important to give this feedback early in the process so this critical functionality of the tool is not omitted.
> Note that F 1.1 requires G 1.1, but G 1.1 also requires F 1.1. Declaring this kind of cycle can be important when singleton functionality moves from one module to another. Our algorithms must not assume the module requirement graph is acyclic.
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?
The minimal version selection addresses a problem that other package managers do not seem to : For transitive dependencies, it allows projects to use the version that direct dependency was actually tested with. This sounds like a good thing to have.
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.
I believe this will get the latest of all the dependencies, not just the direct ones. If my module A depends on B and C, which both in turn depend on D, i want the latest of B and C only, and then transitively pull in the minimal version D compatible with B and C.
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
It's revolutionary like garbage collection was so in the late '90s and functional programming in 2010s: every decade a popular language or library reinvents sth. from two decades ago and it's revolution and unicorns. Now go has it's own Gemfile.lock/requirements.txt thing, and it's revolution. Maybe next revolution will be the "CGAN"...
Definitely not. Nothing here is particularly novel, `vgo` just stakes out a different position on all of the relevant spectrums than most (all) other package managers.
> it's a viable packaging system that isn't NP-hard. Is there any precedent for that?
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.
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.
If you take a moment to read the linked comment, you will see that it describes how Cargo's selection algorithm is a simple heuristic that avoids NP-hardness. The subsequent timing example exists to demonstrate that the heuristic actually succeeds at avoiding pathological performance; Servo has more transitive dependencies than any known Rust project, and your development box is certainly more powerful than my $5 VPS.
> 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.
Why do you suspect it does? Cargo has gotten tons of use and the speed of the dependency resolution process has, to my knowledge, never been a problem.
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).
No good reason, really :-) -- just a gut feeling/bias that when problems move from "tricky" to "easy", people end up creating new/unexpected tooling as a result, just because it's easier.
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.
Your condescension here is amusing. My good friend, please feel free to explain in more detail how the algorithm described in the aforementioned link is miles away from the algorithm in the OP. Feel free to also explain your fascination with linear time algorithms, considering that the nonlinear algorithm in question is demonstrably instantaneous for extreme real-world workloads (Go is supposed to be about pragmatism, I hear?). Lastly, feel free to lecture me further on how a you suspect that an algorithm that has been deployed and used, to overwhelming acclaim, by tens of thousands of programmers over the past three years will be found wanting in comparison to an algorithm that has existed on paper for two days, with no ecosystem or real-world experience to its name. :)
The lack of a solver is predicated on the minimal version decision as well as insisting that breaking changes are reflected in dependency identifiers. These decisions are points on their respective spectrums that other package managers have considered and rejected, because they impose (optimistically) novel or (pessimisticly) burdensome requirements on package authors and consumers. Namely, that we bias toward stable (unchanging) builds, even when security or bugfix releases may be safely applied; and that we force authors toward uncommon and arguably awkward naming decisions when making breaking changes, essentially shifting work from computers to humans.
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.
What makes you think this design was considered by other package managers (npm, yarn, pip, bundler, cargo, etc.) in the past? Is it documented somewhere?
gofmt is only revolutionary in that it refuses to solve the hard problems that pretty printers for other languages solve. Specifically, it doesn't wrap long lines--ever--even though making visually pleasing line breaking choices is pretty much the hardest part of pretty printing (look at how clang-format deals with it, for instance). This design decision would be a non-starter for almost all other languages, where long lines occasionally happen.
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.)
This is a philosophical difference, not an oversight.
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.
Long lines are annoying in practice! Maybe not in Go, where the language tends not to encourage long lines, but in lots of other languages, having to scroll horizontally is a huge distraction to understanding code.
Everyone in a team should use the same conventions. The idea that everyone eveywhere on the world should use the same style has no practical interest : you don't get any productivity from it.
It's just a philosophical stance, and a rather totalitarian one IMHO.
The idea that everyone eveywhere on the world should use the same style has no practical interest : you don't get any productivity from it.
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).
> and removes a whole area which people waste a lot of time on (bikeshedding formatting issues).
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 …
But by not having a maximum line length, everyone gets to bikeshed where to insert line breaks. Thus gofmt isn't doing its job.
(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.)
A pretty printer, historically—going all the way back to indent—allowed the user to define a bespoke style guide, with a specific indent level, with one way to align braces, etc. You could implement GNU-style C code with indent; you could implement Allman-style C code; you could mix in your own preference for indent level.
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.
The purpose of a pretty printer is to increase readability of code. (This isn't controversial.) One of the most important aspects of code readability is line length. By not addressing that, gofmt isn't fully solving the problem that pretty printers are designed to solve.
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".
> 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!
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.
gofmt did not intend to solve the problem that pretty printers are designed to solve, but rather to unify formatting to remove formatting contention within the ecosystem.
A typical pretty printer has knobs and dials to control its output, for instance — gofmt has none.
I think you are missing the point of gofmt, which is to establish a one-to-one correspondence between the source and the AST. This way, you can parse the source to an AST, modify the AST, and reconvert the AST to source, and you have a guarantee that the resulting diff only touches the modified parts.
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):
Personally, I think long lines should never be hard-wrapped; let the editor wrap them.
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.)
It's clearly been done before. Python, for example, has mandatory indentation, so there's huge pressure for everyone to indent the same so that code can be copied around. Going back further, you have languages like Fortran in which column numbers were significant.
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.
Python code is not even close to consistently formatted! You see people using 2 space indents, 4 space indents, tabs...
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.
> 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.
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.
(That's cool to hear about clang-format; I've been out the C++ game for a while, except for the occasional nostalgic prog comp.)
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.)
> 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.
We were using indent with CVS pre-commit hooks in 1999, hardly revolutionary.
Obviously it's not, like, a world-shattering problem, but I think there is a real, if small, overhead, to switching between different styles when looking at code.
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.
> [...] instead force everyone to accept long lines.
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.
Fair enough; that's a good point. I'd still argue, though, that gofmt isn't doing what people expect pretty printers to do. Long lines are some of the most common style violations in most projects.
100% agreed. I'm a strong believer that the 4-6 inch column rule from print media applies equally to text in general and that the 80 character rule is a good approximation for this print rule.
Haskell's build tool Cabal also uses a solver that produces a concrete build solution that takes into account dependency bounds. It uses slightly different rules for the solver than what vgo uses.
It could really be a revolution, and it could be very fast as it is proposed and will be made by the Go team. Imagine it originates from a third party.
One thing I don't like is that sometimes when I reference a go library, sometimes it's vendored dependencies are exposed. This forces me to constantly keep my vendor file in sync with theirs.
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?
dep will ignore the library's vendor directory entirely. You don't need to do any of that work yourself. dep will do it for you.
https://github.com/golang/dep
That’s untrue. You will only be able import the library once and that version will be common across your all of your dependencies. Only major version breaks change this equation, but as explained in the third article those imports will have different names and are effectively different libraries.
Major versions of the same library are available, referenced by different paths.
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.
vgo will guarantee that you only receive a single resolved minor version for any given major of a package. Each major will be imported as a different name, e.g. `import “registry.io/my/pkg”` for v1, `import “registry.io/my/pkg/v2”, etc. Because vendoring is disallowed by vgo that means that you don’t need to worry about issues like `import “registry.io/pkg”` conflicting with `import “yourlib/vendor/registry.io/pkg”`.
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.
[0] https://github.com/shurcooL/Go-Package-Store#readme