10 ms·
This article could be 1 sentence: “it has sum types” :) I find programming in a language without sum types is like rock climbing in ankle weights. I just can’t
by fnfjfk 3y ago
This article could be 1 sentence: “it has sum types” :)
I find programming in a language without sum types is like rock climbing in ankle weights. I just can’t model many types of data correctly, and I hate using product types for something that is clearly a sum type.
Even if Rust didn’t make any safety guarantees over other languages (although sum types provide safety guarantees through exhaustiveness!) I would still think Rust helps a lot because of that.
I guess Swift and Kotlin are basically “it has sum types” languages too, although they both come with baggage (Apple-only in practice, JVM).
- tines 3y agoThis is a huge deal, I find it so difficult to choose a language that doesn't have sum types and destructuring via pattern matching for the kinds of projects I like to work on (compiler/interpreter implementation).
- flohofwoe 3y agoIMHO the one downside with sum types in a performance-oriented systems programming language (or as I prefer to call them 'tagged unions' because that makes it clearer how they actually look like in memory) is that they are inherently cache-unfriendly, because they always take up as much space as the largest wrapped type (plus 8 bytes for the tag). When you have an array of those you need to be aware of the potential 'waste' between array items. (now of course an automatic transformation into a structure-of-arrays would be pretty cool, I actually wonder what Zig's MultiArrayList does with tagged unions). PS: in general I agree though, for instance in Typescript they are fantastic, but there I usually don't care much about memory layout and performance.
- eddd-ddde 3y agoIf you are dealing with always-same-type arrays then you can express so as a sum type of arrays, not an array of sum types.
- widdershins 3y agoThat depends on the sizes of the objects. If the objects in the union are generally small, cache coherence is better than if you have polymorphic objects allocated on the heap.
- soulbadguy 3y agoa good allocator could mitigate most of the allocation effects
- flohofwoe 3y agoUnlikely, because the allocator would need to know things only the compilers knows, and the compiler in turn needs to know how the allocator lays out the data in memory? Some of that could probably be solved with comptime code (like Zig's MultiArrayList container I mentioned above).
- soulbadguy 3y ago> Unlikely, because the allocator would need to know things only the compilers knows Such as ?
- flohofwoe 3y agoSuch a 'smart allocator' can't just work with a size and alignment, it also needs to know the internal structure of objects so that it can arrange them differently (maybe to remove padding bytes, maybe to turn arrays-of-structs into structs-of-arrays. And in turn the compiler needs to know how the 'allocator' has rearranged things in order to generate the code which accesses the data. I also wasn't thinking about moving parts of a struct out into heap allocations, I think in most situations that would be worse than having to deal with padding. Instead at least have the programmer think about how the internal memory layout of sum types might affect performance in specific situations instead of just using them everywhere and in mass because they are convenient.
- gpderetta 3y ago
- throwaway894345 3y agoWhat's the alternative to sum types? What does a "structure of arrays" look like as it pertains to sum types? Are the arrays sparse or dense (and how is either going to be better for cache coherence)? Where does the tag live? In its own array?
- morelisp 3y ago> What does a "structure of arrays" look like as it pertains to sum types? https://news.ycombinator.com/item?id=36195425 https://news.ycombinator.com/item?id=36195425
- imtringued 3y agoUnfortunately, that isn't what cache unfriendly means. You're concerned about wasting memory bandwidth. Your array is trivial to prefetch.
- flohofwoe 3y agoAFAIK the prefetcher doesn't help much if it's mainly busy with prefetching padding bytes of the 'inactive' unions (and I think even a smart prefetcher would at least always load whole cache lines which could be packed with active data instead). Of course that's also a bandwidth problem, you're wasting bandwidth loading data into the cache which then is ignored.
- CodesInChaos 3y agoThe overhead isn't always 8 bytes. When the type uses the naive representation, the overhead will generally match the alignment. For example an Option<u8> occupies 2 bytes, 1 of which is for the tag. And if the type has some illegal values, these can sometimes be used to discriminate the type. When that's possible, the union will have the same size as the largest underlying type with no overhead for a tag. For example Option<&T> matches the size of &T, since it uses the illegal null value to represent None.
- jackmott42 3y agoRust sum types do not take up that much extra space, in fact often the overhead is completely compiled away. Lots of hard work to keep the wasted memory to a minimum, and they are not pointers, so there isn't an extra memory hop or anything.
- mikebenfield 3y ago> plus 8 bytes for the tag The tag can often be stored inline in the data, so doesn't necessarily require extra space. > because they always take up as much space as the largest wrapped type Yeah... but what's the alternative?
- estebank 3y agoThe alternative would be for Rust to generate machine code for the following enum Foo { Bar, Qux([i32; 1024]), } as if the user had written enum Foo { Bar, Qux(Box<[i32; 1024]>), } I don't think it's a good idea to do that kind of silent transformation, instead teach the developer about their options. That way the application's performance doesn't suddenly shift after changing a few parts of unrelated code that accidentally causes some threshold to be crossed that changes the performance characteristics of binary being produced. Today, clippy does what I think is the right thing: it teaches the user warning: large size difference between variants --> src/main.rs:1:1 | 1 | / enum Foo { 2 | | Bar, | | --- the second-largest variant carries no data at all 3 | | Qux([i32; 1024]), | | ---------------- the largest variant contains at least 4096 bytes 4 | | } | |_^ the entire enum is at least 4100 bytes | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#large_enum_variant = note: `#[warn(clippy::large_enum_variant)]` on by default help: consider boxing the large fields to reduce the total size of the enum | 3 | Qux(Box<[i32; 1024]>), | ~~~~~~~~~~~~~~~~
- chriskrycho 3y agoWhen you combine this with rust-analyzer's hints for the sizes of items, it makes it much easier to spot this kind of stuff and resolve it automatically while developing, too.
- deleted 3y ago[deleted]
- RhodesianHunter 3y ago> hey both come with baggage (JVM). I'm always surprised to hear people say this, when my perspective on the JVM is that it's a modern technical marvel several decades in the making with advantages like several unparalleled GCs and an ecosystem with extensive off-the-shelf & highly performant code available for any domain.
- fnfjfk 3y agoI don’t work on server code, and I think a lot of discussions are really focused on server-side work. I have done desktop, mobile, and developer tools (CLIs and “scripts”). If I was working on a server thing Kotlin would seem really good. In my work it is obviously applicable to Android, but not so much elsewhere. I think Rust is nearly perfect for developer CLIs, you don’t really need to deal with the thorny memory and lifetime stuff because most things are allocated on the stack and the entire program is synchronous. clap is very good. Performance, especially startup performance, is incredible.
- square_usual 3y agoYou can use GraalVM Native Image and Kotlin/Native to compile Kotlin to a binary executable, which is fairly fast to start up but does not have all of the high-performance JVM features.
- throwaway894345 3y agoJVM is a marvel at making OOP code merely 10% or 50% slower than C, but I find procedural/data-oriented programming to be quite a lot clearer, faster, and more predictable (with respect to performance) and I don't need nor a huge runtime nor an expert to tune it nor a complicated toolchain to bundle it in with my application (or otherwise ensure that the properly tuned runtime is installed on my target).
- hedora 3y agoTo get it to that level of performance, you need to have expert java programmers, and the result will be hard to maintain. For instance, you can’t use String on hot paths. Also, NPEs can happen almost anywhere, and the compiler doesn’t enforce thread safely (by which I mean “statically checked to be data race free”) Also, the 10-50% slower rule of thumb only applies to throughput. Tail latency is generally much worse than that.
- Night_Thastus 3y agoAs a C++ dev I really don't follow. I don't remember any times where I thought something like a Union would help much. Where have you found so much use for them?
- fnfjfk 3y agoOption/Optional/Maybe is the most obvious one, as a basic example have you interacted with a API that sends some JSON where a field can be null or not? How would you represent that and distinguish it from a field that is always a string?
- Thaxll 3y agoNot many options, pointers most likely.
- TillE 3y agoThat's just std::optional<> vs std::string, isn't it?
- fnfjfk 3y agoYes, and std::optional is a janky implementation of a sum type in a language that doesn’t have them. The ergonomics are bad, and it is easy to make your program blow up if you forget a check. In a language with sum types like Rust, Kotlin, and Swift, if you’re following the default “happy path” of coding, you are not allowed to forget a check. The compiler will not let you.
- chriskrycho 3y agoThey're extremely useful in capturing the "business rules" for a given domain, particularly when it's a restricted domain: if something can only be `A | B | C`, being able to make that explicit, have it checked exhaustively everywhere by the compiler, and be able to dispatch on it easily using `match` is really powerful. That's because it comes with a statically-tracked "tag" (so: sometimes called a "tagged union") that the compiler can use to check you. You can write the same thing with a `union` in C or C++, with an `enum` to tag it, but you're doing that by hand rather than having it built in. Also: Where a struct or class's relationship over its members is the product of all of the types which make it up, a tagged union/enum is the sum of them. A common example: instead of a bunch of optional fields on a single class to model the different possible combinations, modeling something as only the legal set of combinations of fields. I recommend Scott Wlaschin's 'designing with types' series (uses F♯, but applies to any languages with 'sum types') for a great intro and then deep dive on the ideas here: https://fsharpforfunandprofit.com/series/designing-with-types/ https://fsharpforfunandprofit.com/series/designing-with-type...
- josephg 3y agoSum types are fantastic and I’ll join you singing their praises all day long. But this article is about rust’s memory model & memory safety - which is independent of sum types. Unsafe blocks in rust are the only place you can dereference pointers. There are whole classes of memory bugs which are impossible to code up in safe rust code because of this. As an example of how this helps, a couple years ago I was working on a complex & clever rope data structure. My code segfaulted - and I can count on one hand the number of times my rust code has ever done that. A segfault like that can only come from memory corruption or dereferencing invalid memory - both of which are only possible in unsafe code. And the bottom of the stack trace was a safe function, so I knew I’d corrupted memory somehow. These bugs are really hard to find in C - basically the worst case for debugging because the problem could be literally anywhere. But in rust I knew the bug was in one of only two unsafe blocks in the ~thousand or so lines of my library. And one of those blocks was executed not long before the crash. 5 minutes later I had found and fixed the bug. In C, I’ve had similar problems take hours to days to fix. Bugs like this aren’t possible at all in Go or C# because of their memory model. But in those languages you pay for safety by sacrificing performance. The rust I’ve written & benchmarked so far somehow faster than the equivalent C code. Which is pretty wild!
- soulbadguy 3y ago> These bugs are really hard to find in C - basically the worst case for debugging because the problem could be literally anywhere. > 5 minutes later I had found and fixed the bug. In C, I’ve had similar problems take hours to days to fix. Modern tooling like a memory synthesizer and good debugger makes this kind of debugging relatively easy. In some case, the bug would have been detected even before you run with proper warnings. > Bugs like this aren’t possible at all in Go or C# because of their memory model. Except in their respective FFI or unsafe block of code. > But in those languages you pay for safety by sacrificing performance. That's a bit of a strong statement. Even if we accept (i don't) the performance aspect, GC languages give more safety,convenience and dev prod. than rust and at a significantly less complexity overload. > As an example of how this helps, a couple years ago I was working on a complex & clever rope data structure. My code segfaulted - and I can count on one hand the number of times my rust code has ever done that. This might simply be an indication that you were using the wrong tools in the first language. Most native languages (D, C++ etc...) have facilities to avoid unsafe behavior. Use them.
- kshahkshah 3y agoprovided the language later lets you split them apart and declare a branch of code as only dealing with one subtype
- jzackpete 3y agoThis is why I wish there was more momentum behind Hare[1]. It's basically C with sum types, namespaces, and other conservative improvements on C. [1] https://harelang.org/ https://harelang.org/
- skitter 3y agoEh, Hare expects you to write and rewrite collections like hashmaps yourself. It just looks like a less flexibe and less useful Zig.
- jzackpete 3y agoAnd more complex, in implementation and usage. I like Zig too but I don't think there will ever be very good IDE integration (navigation, find usages, refactoring) given its flexibility with regard to the comptime stuff.
- farmerbb 3y agoKotlin also runs on JS and Wasm these days, not just the JVM.
- za3faran 3y agoJava has sum types for a while now, and quite ergonomic with records.