Live data from Hacker News

Sum Types in Julia and Rust

andreaskroepelin.de

51–60 of 70 posts

Re: Sum Types in Julia and Rust

#51
post #3

I may be wrong, but I don't think this is what people mean by 'sum types'. I was expecting union types (e.g., https://www.typescriptlang.org/docs/handbook/unions-and-inte... or https://dotty.epfl.ch/docs/reference/new-types/union-types.h... ) I guess enums are a type of sum type, but seems this is more specifically about enums

I guess technically what people want us algebraic types. Sums of products. Just sums alone are just enums.

Re: Sum Types in Julia and Rust

#52
post #6

Earlier quoted context omitted.

I believe another word for enums (in the Rust sense, not in the C++ or Java sense) are tagged unions.

That's right. It's unfortunate that Rust calls them "enums", since as you noted that term already had a well-established meaning in other languages, and the concept that Rust calls "enums" also had several existing names (sum type, coproduct type, disjoint union, and, more generally, algebraic data type) in the literature and in prior languages. Language designers: stop changing the meanings of words! I know you mean…

> It's unfortunate that Rust calls them "enums"

Rust calls them enum to be familiar to people coming from C-family languages, which is a large target.

> since as you noted that term already had a well-established meaning in other languages

Rust enums "degenerate" to a C-style enum (except typesafe) as it can be repr'd to a number and it's possible to select the discriminant (if there's no associated data).

> the concept that Rust calls "enums" also had several existing names (sum type, coproduct type, disjoint union, and, more generally, algebraic data type) in the literature and in prior languages.

Pretty much none of which are actually part of the language e.g. in Haskell or OCaml the designator is `type`, and it's used for both sum types and product types: the sum type simply has a single constructor.

But Rust doesn't use `type`, it uses `struct`. And a `struct` with multiple variants doesn't make sense.

Re: Sum Types in Julia and Rust

#53

Earlier quoted context omitted.

That's right. It's unfortunate that Rust calls them "enums", since as you noted that term already had a well-established meaning in other languages, and the concept that Rust calls "enums" also had several existing names (sum type, coproduct type, disjoint union, and, more generally, algebraic data type) in the literature and in prior languages. Language designers: stop changing the meanings of words! I know you mean…

Rust enums are also enums: enum Colour { Red, Blue, Green, } is fine.

More than that:

    #[derive(Debug)]
    enum Colour {
        Red = 1,
        Blue = 2,
        Green = 4,
    }

    fn main() {
        println!("{:?}", Colour::Blue);
        println!("{}", Colour::Blue as u8);
    }

Re: Sum Types in Julia and Rust

#54
post #3

I may be wrong, but I don't think this is what people mean by 'sum types'. I was expecting union types (e.g., https://www.typescriptlang.org/docs/handbook/unions-and-inte... or https://dotty.epfl.ch/docs/reference/new-types/union-types.h... ) I guess enums are a type of sum type, but seems this is more specifically about enums

> I may be wrong, but I don't think this is what people mean by 'sum types'.

It is.

> I was expecting union types (e.g., https://www.typescriptlang.org/docs/handbook/unions-and-inte.... or https://dotty.epfl.ch/docs/reference/new-types/union-types.h.... )

Union types are basically an anonymous form of sum types. In the same way tuples and structs / records are both forms of product types.

Statically typed languages which support sum types generally only support the named version (because it tends to be more useful and powerful).

> I guess enums are a type of sum type, but seems this is more specifically about enums

Depends on the language, C's enums are not types at all, Java's or C++'s are product types. Technically enums (or more generally tagged unions, which is what Rust's enums are) is a superset of sum types as you can have multiple variants of the same type, but that's not leveraged here.

Re: Sum Types in Julia and Rust

#55
post #50

Earlier quoted context omitted.

Exponential types are functions [1]. The type A -> B could be written B^A. Interestingly, if you take functions to be lookup tables, then that's related to the number of possible unique lookup tables. First, notice that the type A + B has |A| + |B| values, the type A × B has |A| + |B| values and then we can see that A -> B has |B|^|A| values by remembering that functions can be seen as binary relations with the prope…

Interesting! Now I wonder what "tetrated" types correspond to [1] [1] https://en.wikipedia.org/wiki/Tetration

Well, I don't know of any type theory that has that but you could make something up. C^(B^A) = (B^A) -> C = (A -> B) -> C. So you'd get nested higher-order monomorphic functions like if the base is A and the height |B| = 5 then the result of tetration would be (((A -> A) -> A) -> A) -> A. I don't really know what that means though. Maybe an expert Haskeller could recognize a pattern.

Re: Sum Types in Julia and Rust

#56
post #46
post #38

Earlier quoted context omitted.

But how can the PlayerClass trait methods get extended in the future without opening them back up to add cases to the match expression?

I'm slightly confused. First, PlayerClass is not a trait. And the methods would have to be edited when you add a case to the enum. Is that what you're asking? It's different than if you were implementing those methods on a trait and just had several types that implemented the trait. But the article does show the potential complications that sometimes arise with Rust traits. The difference is that a trait is "open" an…

Sorry, somehow my brain had thought impl and trait went hand in hand, I see that's incorrect.

My question is just - do people really accept this tradeoff? The Rust advice in the article seems to create a maintenance burden compared to the Julia version. Every time you want to extend the system you have to go back and crack open old code. Is the performance difference really that great? Is this really the default approach for Rust programmers? Not snarking about Rust at all, I'm fascinated, but the open-closed principle would be front of mind for me when deciding on these sorts of abstractions (both as a language designer and just a developer).

Re: Sum Types in Julia and Rust

#57

Earlier quoted context omitted.

A sum type is a tagged union [1]. In C, a sum type can be modeled by the combination of an enum and a union. The enum would tell you which field of the union you should use. In Rust, enum's subsume unions and enums of C. This is analogous to a a disjoint union [2] in set theory. Notice that in both cases you have a pair of data. Sum types and disjoint unions are both instances of coproducts in category theory, by the…

According to the tagged union Wikipedia you referenced, a disjoint union is also considered a tagged union. Whether or not that's actually a correct description within the vernacular of computer science, I don't know. "In computer science, a tagged union, also called a variant, variant record, choice type, discriminated union, disjoint union , sum type or coproduct, ..."

They're all what you call isomorphic to eachother, which means roughly any adequate mathematical model of one can mapped to any other.

Re: Sum Types in Julia and Rust

#58
> Use subtyping in Julia and enums in Rust. One caveat though: Enums can not be extended from the “outside”. If you plan to let users of your library add new type variants, you will have to use some kind of trait approach.

This extensibility from the outside is the most important idea behind Julia!

For example this is why a generic deep learning library doesn't need any extra implementation details to be able to run on GPU as well. https://fluxml.ai/Flux.jl/stable/gpu/

Re: Sum Types in Julia and Rust

#59
post #56
post #46

Earlier quoted context omitted.

I'm slightly confused. First, PlayerClass is not a trait. And the methods would have to be edited when you add a case to the enum. Is that what you're asking? It's different than if you were implementing those methods on a trait and just had several types that implemented the trait. But the article does show the potential complications that sometimes arise with Rust traits. The difference is that a trait is "open" an…

Sorry, somehow my brain had thought impl and trait went hand in hand, I see that's incorrect. My question is just - do people really accept this tradeoff? The Rust advice in the article seems to create a maintenance burden compared to the Julia version. Every time you want to extend the system you have to go back and crack open old code. Is the performance difference really that great? Is this really the default appr…

Your concerns are not unwarranted. I've done several Rust projects and usually there really isn't any trade-off.

If you want something open for extension, use a trait unless you can prove to yourself that you "can't". If it's closed or unlikely to be extended, use an enum.

Think of it this way: In Rust you have a choice between open and closed (trait vs enum). In Java, you get open (interface) and that's it. Other languages do also have both, like Rust: Swift, Kotlin, apparently Julia.

Traits have a few sharp edges because of the nature of Rust being built around zero-cost abstractions. But, most of the time, they work just like an interface in Java. The snag I most often hit is if one wants to return Self in a trait method. The most obvious way to work around that, IIRC, is to just return a Box. Keep in mind that in many languages, these things actually work similarly except that the language will just happily throw stuff on the heap transparently. Rust requires you to acknowledge that the former case needs to be boxed and likely on the heap, since you can't know its size at compile time.

In this particular case of extending an enum, I don't see it nearly as horrible as you're describing it. Let's look at it from the POV of another implementation in another language (I don't know Julia): You have an interface, Player. That interface defines a couple of methods. You implement three classes that conform to that interface. Now, six months later you come back and want to add a fourth. You make a new class, say that it implements Player and then your IDE/compiler yells at you until you implement those methods. In the Rust-enum version, you have an enum with three variants and a couple of methods, each of which has to match on the variants. Six months later, you want to add a fourth PlayerClass variant. So, you add it and then your compiler yells at you until you implement the fourth match branch in the couple of methods on the class.

That doesn't seem that different.

To answer some of your inner questions: Yes, Rust devs reach for enums a lot. But really only if you don't expect the cases to change much. Writing version 1.0 in Rust usually takes longer than in other languages because Rust is still a low-level language. Rust's performance advantage doesn't matter for most applications. People choose Rust for much more than the performance: its error philosophy, its enums and good matching, etc.

Re: Sum Types in Julia and Rust

#60
post #59
post #56

Earlier quoted context omitted.

Sorry, somehow my brain had thought impl and trait went hand in hand, I see that's incorrect. My question is just - do people really accept this tradeoff? The Rust advice in the article seems to create a maintenance burden compared to the Julia version. Every time you want to extend the system you have to go back and crack open old code. Is the performance difference really that great? Is this really the default appr…

Your concerns are not unwarranted. I've done several Rust projects and usually there really isn't any trade-off. If you want something open for extension, use a trait unless you can prove to yourself that you "can't". If it's closed or unlikely to be extended, use an enum. Think of it this way: In Rust you have a choice between open and closed (trait vs enum). In Java, you get open (interface) and that's it. Other la…

This is an excellent answer, thank you. I suppose the only remaining discomfort I anticipate is if you have an internal abstraction that might one day be exposed externally in a library. That refactoring doesn’t seem fun, but I suppose these things rarely sneak up on you.
Post reply on HN