• 0 Posts
  • 30 Comments
Joined 2 years ago
cake
Cake day: June 12th, 2023

help-circle


  • It also massively helps with productivity

    Absolutely! Types are as much about providing the programmer with information as they are the compiler. A well typed and designed API conveys so much useful information. It’s why it’s mildly infuriating when I see functions that look like something from C where you’ll see like:

    pub fn draw_circle(x: i8, y: i8, red: u8, green, u8, blue: u8, r: u8) -> bool {
    

    rather than a better strongly typed version like:

    type Point = Vec2<i8>;
    type Color = Vec3<u8>;
    type Radius = NonZero<u8>;
    pub fn draw_circle(point: Point, color: Color, r: Radius) -> Result<()> {
    

    Similarly I think the ability to use an any or dynamic escape hatch is quite useful, even if it should be used very sparingly.

    I disagree with this, I don’t think those are ever necessary assuming a powerful enough type system. Function arguments should always have a defined type, even if it’s using dynamic dispatch. If you just want to not have to specify the type on a local, let bindings where you don’t explicitly define the type are fine, but even in that case it still has a type, you’re just letting the compiler derive it for you (and if it can’t it will error).


  • Hmm, sort of, although that situation is a little different and nowhere near as bad. Rusts type system and feature flags mean that most libraries actually supported both tokio and async-std, you just needed to compile them with the appropriate feature flag. Even more worked with both libraries out of the box because they only needed the minimal functionality that Future provided. The only reason that it was even an issue is that Future didn’t provide a few mechanisms that might be necessary depending on what you’re doing. E.G. there’s no mechanism to fork/join in Future, that has to be provided by the implementation.

    async-std still technically exists, it’s just that most of the most popular libraries and frameworks happened to have picked tokio as their default (or only) async implementation, so if you’re just going by the most downloaded async libraries, tokio ends up over represented there. Longer term I expect that chunks of tokio will get pulled in and made part of the std library like Future is to the point where you’ll be able to swap tokio for async-std without needing a feature flag, but that’s likely going to need some more design work to do that cleanly.

    In the case of D, it was literally the case that if you used one of the standard libraries, you couldn’t import the other one or your build would fail, and it didn’t have the feature flag capabilities like Rust has to let authors paper over that difference. It really did cause a hard split in D’s library ecosystem, and the only fix was getting the two teams responsible for the standard libraries to sit down and agree to merge their libraries.


  • I’ll look into OPAM, it sounds interesting.

    I disagree that combining build and package management is a mistake, although I also agree that it would be ideal for a build/package management system to be able to manage other dependencies.

    A big chunk of the problem is how libraries are handled, particularly shared libraries. Nix sidesteps the problem by using a complex system of symlinks to avoid DLL hell, but I’m sure a big part of why the Windows work is still ongoing is because Windows doesn’t resemble a Linux/Unix system in the way that OS X and (obviously) Linux do. Its approach to library management is entirely different because once again there was no standard for how to handle that in C/C++ and so each OS came up with their own solution.

    On Unix (and by extension Linux, and then later OS X), it was via special system include and lib folders in canonical locations. On Windows it was via dumping everything into C:\Windows (and a lovely mess that has made [made somehow even worse by mingw/Cygwin then layering in Linux style conventions that are only followed by mingw/Cygwin built binaries]). Into this mix you have the various compilers and linkers that all either expect the given OSes conventions to be followed, or else define their own OS independent conventions. The problem is of course now we have a second layer of divergence with languages that follow different conventions struggling to work together. This isn’t even a purely Rust problem, other languages also struggle with this. Generally most languages that interop with C/C++ in any fashion do so by just expecting C/C++ libraries to be installed in the canonical locations for that OS, as that’s the closest thing to an agreed upon convention in the C/C++ world, and this is in fact what Rust does as well.

    In an ideal world, there would be an actual agreed upon C/C++ repository that all the C/C++ devs used and uploaded their various libraries to, with an API that build tools could use to download those libraries like Rust does with crates.io. If that was the case it would be fairly trivial to add support to cargo or any other build tool to fetch C/C++ dependencies and link them into projects. Because that doesn’t exist, instead there are various ad-hoc repositories where mostly users and occasionally project members upload their libraries, but it’s a crap-shoot as to whether any given library will exist on any given repository. Even Nix only has a tiny subset of all the C/C++ libraries on it.


  • So, it’s C#?

    No, that’s what Java would look like today if designed by a giant evil megacorp… or was that J++. Eh, same difference. /s

    This did make me laugh though. Anyone else remember that brief period in the mid-90s when MS released Visual J++ aka Alpha C#? Of course then Sun sued them into the ground and they ended up abandoning that for a little while until they were ready to release the rebranded version in 2000.



  • Rusts ownership model is not just an alternative to garbage collection, it provides much more than that. It’s as much about preventing race conditions as it is in making sure that memory (and other resources) get freed up in a timely fashion. Just because Go has GC doesn’t mean it provides the same safety guarantees as Rust does. Go’s type system is also weaker than Rusts even setting aside the matter of memory management.


  • Eww… you’re probably right. TIHI.

    On a related note, I’ve always preferred t-shirt sizing over story points. You can still screw that up by creating a conversion chart to translate t-shirt sized into hours (or worse, man-hours) or story points, but at least it’s slightly more effort to get wrong than the tantalizingly linear numeric looking story points.

    If I was truly evil I’d come up with a productivity unit that used nothing but irrational constants.

    “Hey Bob, how much work do you think that feature is?”

    “Don’t know man, I think maybe e, but there’s a lot there so it might end up being π.”


  • It sort of has nil. While a type can be null or undefined when evaluated, nil is used in many of the JS libraries and frameworks to mean something that is either null or undefined. So you’ll see functions like function isNil(value) { return value == null || value == undefined } and they’ll sometimes often confuse things even more be actually defining a nil value that’s just an alias for null which is just pointlessly confusing.

    As an aside, basically every language under the sun has NaN as it’s part of the IEEE floating point standard. JavaScript just confuses the situation more than most because it’s weakly typed so it doesn’t differentiate between integers, floats, or some other type like an array, string, or object. Hence anything in JS can be a NaN even though it really only has meaning for a floating point value.


  • Yeah as far as I’m concerned null is public enemy number one. I refuse to work in any language that doesn’t allow me to indicate in some fashion that a variable is non-nullable. I just about had an aneurysm when I found out that JavaScript not only has null, but also nil and undefined and they all mean something subtly different. To be fair though, JavaScript is like a greatest hits of bad language design.


  • I think the issue with that is that it’s a little bit of a solution in search of a problem. Your example of:

    fn do_stuff() -> Result<...> {
        if condition {
            return Error(...)
        }
    
        return Ok(...)
    } out (r) {
        if r.is_err() {
            // special cleanup (maybe has access to fn scope vars)
        }
    }
    

    isn’t really superior in any meaningful way (and is arguably worse in some ways) to:

    fn do_stuff() -> Result<...> {
        if condition {
            // special cleanup (maybe has access to fn scope vars)
            return Error(...)
        }
    
        return Ok(...)
    }
    

    For more complicated error handling the various functions on Result probably have all the bases covered.

    For what it’s worth a lot of my day to day professional work is actually in Java and our code base has adopted various practices inspired by Rust and Haskell. We completely eliminated null from our code and use Optional everywhere and use a compile time static analysis tool to validate that. As for exception handling, we’re using the Reactor framework which provides a type very similar to Result, and we essentially never directly throw or catch exceptions any more, it’s all handled with the functions Reactor provides for error handling.

    I just don’t think the potential footguns introduced by null and exceptions are worth it, the safer type level abstractions of Option and Result are essentially superior to them in every way.


  • Right, it’s essentially the same argument as strong vs. weak typing. The weak typing proponents say JavaScript is best, because you can just write anything and you don’t need to worry about all those pesky types getting in your way. The strong typing proponents (which if it’s not obvious I am one of) point out that you can write incorrect code quickly in just about any language, but writing correct code is much harder, and the cost of correcting code increases the later the mistake is found. Errors that can’t even be written are better than errors that are found at compile time which are better than errors that are reliably caught at runtime, which are all infinitely better than errors that only randomly appear under very specific circumstances.

    That is why many people switched to using TypeScript for their websites instead of JavaScript, because even though you have to spend more time putting type annotations on everything, and at the end of the day at runtime TypeScript is literally just JavaScript, the errors it lets you find at compile time instead of runtime make the effort necessary to include those types worth it. Same thing applies with Rust vs. Go. Yes it requires more thinking up front when you’re writing Rust code, and yes it might take you longer to write that code, but it’s also going to be correct code you can be confident in and not have a bunch of ticking timebombs waiting in it that you don’t even know about.

    An extra 30 minutes spent having to think about a dozen lines of code, is infinitely preferable to spending 3 hours pouring over stack traces and single stepping debuggers to find that one subtle mistake you made.


  • My point is that with C++, the programmer must often play a puzzle of avoiding common pitfalls with e.g. memory allocation - on top of the actual problem the programmer is intending to solve.

    Both Rust and Go are more free from this kind of extra mental overhead.

    This isn’t entirely correct. Rust you do still need to worry about those same problems, it just gives you much better abstractions for modeling and thinking about them, and the tooling of the language “checks your homework” so to speak to make sure you didn’t make any mistakes in how you were thinking about it. The upside is that you can be very specific about how you handle certain tasks allowing you to be super efficient with resources. The downside is that you do still need to worry about those resources at least a little bit. If you wanted to you could write Rust like Go by just making every variable a Box<Arc<...>> and using .clone() like mad, but your performance is going to take a hit.

    Go (and other GCed) languages on the other hand, do entirely free you from having to worry about memory utilization in a general sense as the language runtime takes care of that for you. The downside is that it often does a poor job of doing so, and if you do run into one of the edge cases it’s not so great at your tools for dealing with that are severely limited. Further it’s very easy to accidentally screw up your memory usage and use far more than is necessary leading to excessive heap churn and serious performance degradation. Because the language makes it easy, even if what you’re doing is wrong, and it lacks the tools to alert you to that problem before you trip over it at runtime.

    Having a simple and verbose language is not necessarily a downside.

    As programmers, our bread and butter is abstractions. We use abstractions because in a very real sense what we do day to day if we removed all the abstractions would be a herculean effort that not even the best of us could manage for any period of time. Go’s idea of “simple” is limiting the number of abstractions that the language provides, and so it’s up to the programmer to use that small handful to construct more powerful ones. Every code base becomes a snowflake where each team rolled their own solution, or everyone just uses the same sets of libraries that provide the solution. You haven’t removed the complexity, you’ve just shifted it out of the language and onto a 3rd party. It’s better to have a consistent set of abstractions provided and maintained by the language and centrally developed by everyone, rather than a hodge-podge of abstractions by random 3rd parties.


  • Hmm… I think the Rust-y answer to that problem is the same as the Haskell-y answer, “Use the Types!”. I.E. in the example above instead of returning an i32 you’d return a NonZero<u32>, and your args would be a: &NonZero<u32>, b: u32. Basically make invalid state unrepresentable and then you don’t need to worry about the API being used wrong.


  • So you’re point is that your custom home grown workaround to a failure of C++ doesn’t play well with Rusts official solution to the same problem? And therefore Rusts solution isn’t better than C++ lack of a solution?

    While that is unfortunate for you and you certainly seem to have tech-debted yourself into a particularly nasty corner, I’m not sure that logic follows.


  • Probably because Rust doesn’t have exceptions

    Well, it has something semantically equivalent while being more explicit, which is Result (just like Option is the semantic equivalent of null).

    and I’m pretty sure there are no guarantees with panic!().

    I actually do quite a bit of bare metal Rust work so I’m pretty familiar with this. There are sort of guarantees with panic. You can customize the panic behavior with a panic_handler function, and you can also somewhat control stack unwinding during a panic using std::panic::catch_unwind. The later requires that anything returned from it implement the UnwindSafe trait which is sort of like a combination Send + Sync. That said, Rust very much does not want you to regularly rely on stack unwinding. Anything that’s possible to recover from should use Result rather than panic!() to signal a failure state.


  • But in D you can do explicit scope guards

    Hmm… that is interesting.

    scope(exit) is basically just an inline std::ops::Drop trait, I actually think it’s a bad thing that you can mix that randomly into your code as you go instead of collecting all of the cleanup actions into a single function. Reasoning about what happens when something gets dropped seems much more straightforward in the Rust case. For instance it wasn’t immediately clear that those statements get evaluated in reverse order from how they’re encountered which is something I assumed, but had to check the documentation to verify.

    scope(success) and scope(failure) are far more interesting as I’m not aware of a direct equivalent in Rust. There’s the nightly only feature of std::ops::Try that’s somewhat close to that, but not exactly the same. Once again though, I’m not convinced letting you sprinkle these statements throughout the code is actually a good idea.

    Ultimately, while it is interesting, I’m actually happy Rust doesn’t have that feature in it. It seems like somewhat of a nightmare to debug and something ripe to end up as a footgun.


  • Dependency management has to deal with the real world where what we didn’t know in 1970 hurts us.

    I’m having trouble understanding the point you’re trying to make here. You seem to be angry at the Rust dependency manager for not being perfect, but also admit that it’s better than C++. Is there some dependency manager you like more than what Rust provides? Do you have any suggestions for how Rust could improve its dependency management?


  • compile-time function execution - basically write macros in D; I saw some madlads writing a complete shader render loop at compile-time

    There are of course macros, but they’re kind of a pain to use. Zigs comptime fn are really nice and a similar concept. Rust does have const fn but of course those come with limits on them.

    explicit scopes for finalizers - destructors can be run deterministically instead of “eventually” like in many GC languages

    You kind of get that with Rust for free. You get implicit GC for anything stack allocated, and technically heap allocated values are deterministically freed which you can work out by tracking their ownership. As soon as the owning scope exits it will be freed. If you want more explicit control you can always invoke std::mem::drop to force it to be freed immediately, but generally you don’t gain much by doing so.

    really fast compiler

    Some really great work is being done on that pretty much all the time but… yeah, I can’t reasonably argue that the Rust compiler is fast. Taking full advantage of incremental compilation helps a lot, but if you’re doing a clean build, better grab a coffee.

    What would be nice is if cargo explored a similar solution to what Arch Linux used, where there’s a repository of pre-compiled libraries for various platforms and configurations that can be used to speed up build times. That of course does come with a whole heap of problems though, probably the biggest of which is that it’s a HUGE security nightmare. Of lesser concern is the fact that they could not realistically do so for every possible combination of features or platforms, so it would likely only apply to crates built with the default features for a small subset of the most popular platforms. I’m also not sure what the tree shaking would end up looking like in a situation like that.