← Back to context

Comment by robviren

7 days ago

I find the dependency creep for both rust and node unfortunate. Almost anything I add explodes the deps and makes me sweat for maintenance, vulnerabilities, etc. I also feel perpetually behind, which I think is basically frontend default mode. Go does the one thing I wish Rust had more of which is a pretty darn great standard library with total backwards compatibility promises. There are awkward things with Go, but man, not needing to feel paranoid and how much can be built with so little feels good. But I totally understand just getting crap done and taking off the tin foil. Depends on what you prioritize. Solo devs don't have the luxury.

Those deps have to come from somewhere, right? Unless you're actually rolling your own everything, and with languages that don't have package managers what you end up doing is just adding submodules of various libraries and running their cmake configs, which is at least as insecure as NPM or Crates.io.

Go is a bit unique a it has a really substantial stdlib, so you eliminate some of the necessary deps, but it's also trivial to rely on established packages like Tokio etc, vendor them into your codebase, and not have to worry about it in the future.

  • > Those deps have to come from somewhere, right? Unless you're actually rolling your own everything

    The point is someone needs to curate those "deps". It's not about rolling your own, it's about pulling standard stuff from standard places where you have some hope that smart people have given thought to how to audit, test, package, integrate and maintain the "deps".

    NPM and Cargo and PyPI all have this disease (to be fair NPM has it much worse) where it's expected that this is all just the job of some magical Original Author and it's not anyone's business to try to decide for middleware what they want to rely on. And that way lies surprising bugs, version hell, and eventually supply chain attacks.

    The curation step is a critical piece of infrastructure: thing things like the Linux maintainer hierarchy, C++ Boost, Linux distro package systems, or in its original conception the Apache Foundation (though they've sort of lost the plot in recent years). You can pull from those sources, get lots of great software with attested (!) authorship, and be really quite certain (not 100%, but close) that something in the middle hasn't been sold to Chinese Intelligence.

    But the Darwinian soup of Dueling Language Platforms all think they can short circuit that process (because they're in a mad evangelical rush to get more users) and still ship good stuff. They can't.

    • I mean somebody could make a singular rust dependency that re-packages all of the language team's packages.

      But what's the threat model here. Does it matter that the Rust STD library doesn't expose say "Regex" functionality forcing you to depend on Regex [1] which is also written by the same people who write the STD library [2]? Like if they wanted to add a back-door in to Regex they could add a backdoor into Vec. Personally I like the idea of having a very small STD library so that it's focused (as well as if they need to do something then it has to be allowed by the language unlike say Go Generics or ELM).

      Personally I think there's just some willful blindness going on here. You should never have been blindly trusting a giant binary blob from the std library. Instead you should have been vendoring your dependencies and at that point it doesn't matter if its 100 crates totaling 100k LOC or a singular STD library totaling 100k LOC; its the same amount to review (if not less because the crates can only interact along `pub` boundaries). [1]: https://docs.rs/regex/latest/regex/

      [2]: https://github.com/rust-lang/regex

      5 replies →

  • The tradeoff Go made is that certain code just cannot be written in it.

    Its STD exists because Go is a language built around a "good enough" philosophy, and it gets painful once you leave that path.

    • > The tradeoff Go made is that certain code just cannot be written in it.

      Uh... yeah? That's true of basically all platforms, and anyone who says otherwise is selling something.

      > it gets painful once you leave that path

      Still less painful than being zero-day'd by a supply chain attack.

      4 replies →

  • It is more of a cultural thing. Package managers encourage lots of dependencies while programmers using language with no package managers will often pride themselves in having as few dependencies as possible. when you consider the complete graph, it has an exponential effect.

    It is also common in languages without package managers to rely on the distro to provide the package, which adds a level of scrutiny.

  • Technically it's the same. But behaviorally it's not. When pulling in more dependencies is so easy, it's very hard to slow down and ask the question do we need all of this?

    Mucking around with cmake adds enough friction that everyone can take a beat for thoughtful decision-making.

  • > Go is a bit unique a it has a really substantial stdlib

    It’s not that unique though. I can say that Python and hell, even PHP have pretty complete but also well documented stdlib.

    Java is meh tier but C# is also pretty good in this aspect.

    It’s totally a choice for Rust not to have a real stdlib and actually I feel like that would maybe make Rust maybe the best language overall.

Python used to have a great standard library, too. But now it's stuck with a bunch of obsolete packages and the packaging story for Python is awful.

In a decade or so Go the awkward things about Go will have multiplied significantly and it'll have many of the same problems Python currently has.

  • > the packaging story for Python is awful.

    Big caveat that this is just for me personally, but uv has fixed this for me personally. Game changing improvement for Python. Appropriately, uv is written in rust.

    • The fact that you have to know to use uv rather than any of the other package managers is kind of the point.

  • > In a decade or so Go the awkward things about Go will have multiplied significantly and it'll have many of the same problems Python currently has.

    The stdlib packages are far better designed in Go than in Python. “The standard library is where packages go to die” is literally not a thing in Go, in fact quite the opposite.

These are two sides of the same coin. Go has its quirks because they put things in the standard library so they can't iterate (in breaking manners), while Rust can iterate and improve ideas much faster as it's driven by the ecosystem.

Edit: changed "perfect" to "improve", as I meant "perfect" as "betterment" not in terms of absolute perfection.

  • There is a moral hazard here. By accepting that APIs are forever, you tend to be more cautious and move toward getting it right the first time. Slower is better... And also faster in the long run, as things compose. Personally, I do believe that there is one best way to do things quite often, but time constraints make people settle.

    At least it is my experience building some systems.

    Not sure it is always a good calculus to defer the hard thinking to later.

  • The golang.org/x/ namespace is the other half of the standard library in all but name. That gets iterated often.

    For stuff in the standard library proper, the versioning system is working well for it. For example, the json library is now at v2. Code relying on the original json API can still be compiled.

  • Iterating often is not helpful for stable systems over time.

    I like go's library it's got pretty much everything needed out of the box for web server development. Backwards compatibility is important too.

  • The cost of "perfecting" an idea here is ruining the broader ecosystem. It is much much better for an API to be kinda crappy (but stable) for historical reasons than dealing with the constant churn and fragmentation caused by, for example, the fifth revision of that URL routing library that everyone uses because everyone uses it. It only gets worse by the orthogonal but comorbid attitude of radically minimizing the scope of dependencies.

    • Which has been working great for go, right. They shipped "log" and "flag" stdlib packages, so everyone uses... well, not those. I think "logrus" and "zap" are probably the most popular, but there's a ton of fragmentation in Go because of the crappy log package, including Go itself now shipping two logging packages in the stdlib ('log/slog').

      Rust on the other hand has "log" as a clear winner, and significantly less overall fragmentation there.

      1 reply →

    • > It is much much better for an API to be kinda crappy (but stable) for historical reasons

      But this does more than just add a maintenance burden. If the API can't be removed, architectural constraints it imposes also can't be removed.

      e.g. A hypothetical API that guarantees a callback during a specific phase of an operation means that you couldn't change to a new or better algorithm that doesn't have that phase.

      2 replies →

    • I think “the fifth revision of that URL routing library that everyone uses” is a much less common case than “crate tried to explore a problem space, five years later a new crate thinks it can improve upon the solution”, which is what Rust’s conservatism really helps prevent. When you bake a particular crate into std, competitor crates now have a lot of inertia to overcome; when they're all third-party, the decision is not “add a crate?” but “replace a crate?” which is more palatable.

      Letting an API evolve in a third-party crate also provides more accurate data on its utility; you get a lot of eyes on the problem space and can try different (potentially breaking) solutions before landing on consensus. Feedback during a Rust RFC is solicited from a much smaller group of people with less real-world usage.

      1 reply →

The dependency creep keeps on happening in web frameworks where ever you look.

I was thinking of this quote from the article:

> Take it or leave it, but the web is dynamic by nature. Most of the work is serializing and deserializing data between different systems, be it a database, Redis, external APIs, or template engines. Rust has one of the best (de)serialization libraries in my opinion: serde. And yet, due to the nature of safety in Rust, I’d find myself writing boilerplate code just to avoid calling .unwrap(). I’d get long chain calls of .ok_or followed by .map_err. I defined a dozen of custom error enums, some taking other enums, because you want to be able to handle errors properly, and your functions can’t just return any error.

I was thinking: This is so much easier in Haskell.

Rather than chains of `ok_or()` and `map_err()` you use the functor interface

Rust:

``` call_api("get_people").map_or("John Doe", |v| get_first_name(v)).map_or(0, |v| get_name_frequency(v)) ```

Haskell:

``` get_first_name . get_name_frequency <$> callApi "get_people" ```

It's just infinitely more readable and using the single `<$>` operator spares you an infinite number of `map_or` and `ok_or` and other error handling.

However, having experience in large commercial Haskell projects, I can tell you the web apps also suffer from the dreaded dependency explosion. I know of one person who got fired from a project due to no small fact that building the system he was presented with took > 24 hours when a full build was triggered, and this happened every week. He was on an older system, and the company failed to provide him with something newer, but ultimately it is a failing of the "everything and the kitchen sink" philosophy at play in dependency usage.

I don't have a good answer for this. I think aggressive dependency reduction and tracking transitive dependency lists is one step forward, but it's only a philosophy rather than a system.

Maybe the ridiculous answer is to go back to php.

  • Rust has trouble supporting higher-kinded types like Functor (even though an equivalent feature is available, namely Generic Associated Types) due to the distinctions it makes between owned and referenced data that have no equivalent in Haskell. Whether these higher abstractions can still be used elegantly despite that complexity is something that should be explored via research, this whole area is not ready for feature development.

  • > I know of one person who got fired from a project due to no small fact that building the system he was presented with took > 24 hours when a full build was triggered, and this happened every week.

    Incremental Nix builds can take less than 1 munute to build everything, including the final deployable docker image with a single binary on very large Haskell codebases. That fact the the person was fired for everybody around him systematically failing to admit and resolve a missing piece of supportive infrastructure for the engineering effort of one person tells a lot about the overall level of competence in that team.

    > but ultimately it is a failing of the "everything and the kitchen sink" philosophy at play in dependency usage.

    Not really, as the kitchen sink only has to build once per its version change, for all future linkage with your software for the entire engineering team doing the builds in parallel.

  • 24 hours? is the haskel compiler written in javascript running in a python js-interpreter written in bash?

  • php is the only popular language that regularly removes insane legacy cruft (to be fair, they have more insane cruft than almost any other language to begin with).

I've found Go's standard library to be really unfortunate compared to rust.

When I update the rust compiler, I do so with very little fear. My code will still work. The rust stdlib backwards compatible story has been very solid.

Updating the Go compiler, I also get a new stdlib, and suddenly I get a bunch of TLS version deprecation, implicit http2 upgrades, and all sorts of new runtime errors which break my application (and always at runtime, not compiletime). Bundling a large standard library with the compiler means I can't just update the tls package or just update the image package, I have to take it or leave it with the whole thing. It's annoying.

They've decided the go1 promise means "your code will still compile, but it will silently behave differently, like suddenly 'time1 == time2' will return a different result, or 'http.Server' will use a different protocol", and that's somehow backwards compatible.

I also find the go stdlib to have so many warts now that it's just painful. Don't use "log", use "log/slog", except the rest of the stdlib that takes a logger uses "log.Logger" because it predates "slog", so you have to use it. Don't use the non-context methods (like 'NewRequest' is wrong, use 'NewRequestWithContext', don't use net.Dial, etc), except for all the places context couldn't be bolted on.

Don't use 'image/draw', use 'golang.org/x/image/draw' because they couldn't fix some part of it in a backwards compatible way, so you should use the 'x/' package. Same for syscall vs x/unix. But also, don't use 'golang.org/x/net/http2' because that was folded into 'net/http', so there's not even a general rule of "use the x package if it's there", it's actually "keep up with the status of all the x packages and sometimes use them instead of the stdlib, sometimes use the stdlib instead of them".

Go's stdlib is a way more confusing mess than rust. In rust, the ecosystem has settled on one logging library interface, not like 4 (log, slog, zap, logrus). In rust, updates to the stdlib are actually backwards compatible, not "oh, yeah, sha1 certs are rejected now if you update the compiler for better compile speeds, hope you read the release notes".

  • Man, I've been using Go as my daily driver since 2012 and I think I can count the number of breaking changes I've run into on one finger, and that was a critical security vulnerability. I have no doubt there have been others, but I've not had the misfortune of running into them.

    > Don't use "log", use "log/slog", except the rest of the stdlib that takes a logger uses "log.Logger" because it predates "slog", so you have to use it.

    What in the standard library takes a logger at all? I don't think I've ever passed a logger into the standard library.

    > the ecosystem has settled on one logging library interface, not like 4 (log, slog, zap, logrus)

    I've only seen slog since slog was added to the standard library. Pretty sure I've seen logrus or similar in the Kubernetes code, but that predated slog by a wide margin and anyway I don't recall seeing _any_ loggers in library code.

    > In rust, the ecosystem has settled on one logging library interface

    I mean, in Rust everyone has different advice on which crates to use for error handling and when to use each of them. You definitely don't have _more standards_ in the Rust ecosystem.

    • > I don't think I've ever passed a logger into the standard library.

      `net/http.Server.ErrorLog` is the main (only?) one, though there's a lot of third-party libraries that take one.

      > I've only seen slog since slog was added to the standard library

      Most go libraries aren't updated yet, in fact I can't say I've seen any library using slog yet. We're clearly interfacing with different slices of the go ecosystem.

      > in Rust everyone has different advice on which crates to use for error handling and when to use each of them. You definitely don't have _more standards_ in the Rust ecosystem.

      They all are still using the same error type, so it interoperates fine. That's like saying "In go, every library has its own 'type MyError struct { .. }' that implements error, so go has more standards because each package has its own concrete error types", which yeah, that's common... The rust libraries like 'thiserror' and such are just tooling to do that more ergonomically than typing out a bunch of structs by hand.

      Even if one dependency in rust uses hand-typed error enums and another uses thiserror, you still can just 'match' on the error in your code or such.

      On the other hand, in Go you end up having to carefully read through each dependency's code to figure out if you need to be using 'errors.Is' or 'errors.As', and with what types, but with no help from the type-system since all errors are idiomatically type-erased.

  • > When I update the rust compiler, I do so with very little fear. My code will still work. The rust stdlib backwards compatible story has been very solid.

    This is not always true, as seen with rustc 1.80 and the time crate. While it only changed type inference, that still caused some projects like Nix a lot of trouble.

    • That caused compilation errors though, which are alright in my book, and don't increase my fear to update.

      Silent runtime changes are what spook me and what I've gotten more often with Go.

FYI: Deno includes:

1. The web standard APIs themselves 2. It's own standard library inspired by Go's standard library (plus some niceties like TOML minus some things not wanted in a JS/TS standard library since they're already in the web standard APIs) 3. Node's standard library (AKA: built-in modules) to maintain backwards compatibility with node.

Bun has 1 and 3, and sort of has it's own version of 2 (haphazard, not inspired by go, and full of bun-isms which you may like but may not, but standard database drivers is nice).

Honestly this is one of the biggest reasons I stick with Elixir. Between Elixir’s standard library, the BEAM/OTP, and Phoenix (with Ecto)—- I honestly have very few dependencies for web projects. I rarely, at this point, find the need to add anything to new projects except for maybe Mox (mocking library) and Faker (for generating bits of test data). And now that the Jason (JSON) library has been more or less integrated into OTP I don’t even have to pull it in. Elixir dev experience is truly unmatched (IMHO) these days.

A Tauri hello world app has about 500(?) deps out of the box, always makes me laugh.

I get that cross platform desktop app is a complicated beast but it gives off those creepy npm vibes.

With Go it's good to keep in mind the Proverbs, which includes this gem:

  A little copying is better than a little dependency.

  • Good luck, if little copying is ICU based localization.

    • The whole point of "a little copying" is when there's self-contained code that can be copypasta'd -- for situations that are appropriate.