Live data from Hacker News

Algebraic Data Types for C99

github.com

231–233 of 233 posts

Re: Algebraic Data Types for C99

#231

Earlier quoted context omitted.

This doesn't have anything to do with ADTs. It's because Rust has both in-place variables and references. You aren't changing the variant of an existing ADT, you're replacing the entire ADT value with a new one. That replacement is visible in multiple places because the language allows you to take references to variables (the `&mut ex` expression). You can accomplish the same thing in C and C++ because they also have…

> You aren't changing the variant of an existing ADT, you're replacing the entire ADT value with a new one. 'Values' in Rust have no identity, except for their address. They're just a bunch of bytes in a row. What could it mean for a value to exist, except for it to be present at a set place in memory? If I have an instance of a Java class, and I change all the fields, I'd hardly say the instance has been replaced wi…

What you're describing is pointers and value semantics. Rust (and C and C++ and to some degree Go) has those. Java does not.

Pointers and value semantics are nice, but have nothing to do with ADTs. For example, in Rust, you can also do:

    let mut ex: i32 = 42;
    println!("{ex:?}"); // 42

    let ex_ref: &mut i32 = &mut ex;
    *ex_ref = 53;

    println!("{ex:?}"); // 53
And in C:

    int x = 42;
    printf("%d\n", x); // 42

    int* x_ref = &x;
    *x_ref = 53;

    printf("%d\n", x); // 53
In these examples, we haven't mutated the number 42 into the number 53. We've simply stored an entirely new value in the location of `x`. In your Rust example, you're doing the exact same thing with an ADT. The variant case isn't being changed. You're creating a new value and storing it in an existing storage location. Every pointer pointing to that storage location sees the update because they all point to the same storage.

Re: Algebraic Data Types for C99

#232
post #188
post #137

Earlier quoted context omitted.

No, it isn't "completely useless". If you have a function that will normally return a string, but can sometimes fail due to reasons, you may wish to yield an error message in the latter case. So you're going to be returning a string, or a string. It's not what the content of the data is; it's how you're supposed to interpret it. You have two cases, success and failure, and control will flow differently depending on t…

No disrespect, but that still sounds entirely useless to me. I would never model something as `String | String` as that makes zero sense. You should use a `Result` or `Either` type for that like everyone does.

> You should use a `Result` or `Either` type for that like everyone does.

You have missed the thread context, which is whether `Either a a` (also written `a + a`) has any merits over simply `a` (which is identical to `a | a`). If you're on the `Either` train already, we are arguing over imaginary beef.

> No disrespect, but that still sounds entirely useless to me.

It is disrespectful to say something "makes zero sense", regardless of anything you might say to the contrary. You've misrepresented my point: nobody wants to model something as `String | String`.

If you have, say, an `Int | Bool`, and you pass both sides through some kind of stringification function, you're naturally going to get `String | String` without ever having written that type down yourself. You wouldn't necessarily want to collapse that to `String`, however, because you may -- for instance -- want to give strings and ints different decorations around them before finally flattening them. You might write such a function as something like

    (ib) => ib
      .mapBoth(showInt, showBool)
      .visit(prepend("int: "), prepend("bool: "));
You couldn't run this if the result of the `mapBoth` had type `String | String`: that type is indistinguishable from `String`, and since you can't tell which case you're in, you wouldn't know which tag to prepend.

You could write the same function without passing through `String | String`:

    (ib) => ib.visit(
      sequence(showInt, prepend("int: ")),
      sequence(showBool, prepend("bool: ")));
And yes, in this especially contrived example, perhaps you may find that to make more sense anyway. But in longer pipelines, sometimes separated over multiple functions, often involving generic code, it becomes much harder to simply always fuse steps in this manner. This is why we say sum types (e.g. `String + String`) compose better: they don't behave any differently depending on whether the two sides are the same or not. You have explicit control over when the two sides rejoin.

Re: Algebraic Data Types for C99

#233
post #137

Earlier quoted context omitted.

No, it isn't "completely useless". If you have a function that will normally return a string, but can sometimes fail due to reasons, you may wish to yield an error message in the latter case. So you're going to be returning a string, or a string. It's not what the content of the data is; it's how you're supposed to interpret it. You have two cases, success and failure, and control will flow differently depending on t…

Why would you return `string | string` here? Wouldn't you explicitly mark the error, and return `string | error`? (substitute error with whatever type you want there - null, Error, or your own thing)

> substitute error with whatever type you want there - null, Error, or your own thing

And if I want `error` to be `string` so I can just provide an error message? Proper disjoint sum types let me keep the two sides disjoint, even if they happen to be the same type. Union types will collapse on any overlap, so if I happen to instantiate `error` at `string` then I can no longer tell my error case from my data case.

Post reply on HN