carbon-language / carbon-lang

Carbon Language's main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)
http://docs.carbon-lang.dev/
Other
32.42k stars 1.48k forks source link

Forward declarations of choice types #3652

Open zygoloid opened 10 months ago

zygoloid commented 10 months ago

Currently, there is no suggestion in the design that choice types support forward declarations, and the toolchain implementation consequently does not support forward declarations of choice types. So:

I don't think there are any novel issues with choice type declarations that don't apply to classes. But the second question seems like it might deserve a little thought.

geoffromer commented 10 months ago

Presumably the syntax will require the forward declaration to use class or choice, so the question is whether the keyword used has to match the definition.

If a sum-type class could be forward-declared with choice, that could make it easier to evolve a choice type by replacing it with a class implementation of the same API. However, I'm not sure how important that will be in practice. If I understand correctly, forward declarations will only be possible within a package, so if packages are sufficiently small it may be feasible to change all the forward declarations as part of the implementation change.

If we do allow using choice to forward-declare a class definition, I think it should be limited to classes that actually implement the sum type interface, because it would be really confusing if you could use choice to forward-declare arbitrary classes.

Even with that restriction, it seems a little messy to have cases where there's an arbitrary, formally meaningless choice between choice and class. I'm almost tempted to suggest that in general we require types to be forward-declared with class, regardless of the keyword used to define them. However, that seems liable to be a future FAQ entry ("Why can't I forward-declare my choice type?"), so it's probably better to avoid that.

zygoloid commented 10 months ago

One interesting consequence of changing the adapter syntax from adapter X for Y { ... } to class X { adapt Y; ... } is that we get to forward-declare adapters with class X;. I wonder if the same logic should be applied to choice types: if we want choice types to notionally be a particular way to define a class, then perhaps they should use syntax beginning with class X. Then the forward declaration question becomes pretty trivial -- you can forward-declare with class X;, because that's how you forward-declare any class type.

Syntactically, our approach up to now has been that the portion of the definition from the { onwards can be replaced by a ; to form a declaration, which would suggest something like:

class Optional(T:! type) {
  choice {
    Some(t: T),
    None
  }
}

... but given that Some and None are (among other things) factory functions, perhaps something more similar to a function declaration (eg, with choice replacing fn) would be better:

class Optional(T:! type) {
  choice None;
  choice Some(t: T);
}

This gives us immediate syntactic support for putting access specifiers on the alternatives, adding other member names including member functions to choice types, and so on. (Presumably we wouldn't permit a class to have both var fields and choice alternatives, just like we don't permit it to have both var fields and an adapt declaration.)

jonmeow commented 10 months ago

Regarding the latter syntax in particular, I tend to think of the union format that choice represents as more ordered across name than fn. For example, in C++ if I write enum class Boolean : uint8_t { False, True };, that is meaningfully distinct from enum class Boolean : uint8_t { True, False }; because I can also make use of the integer values underlying the enum. Would Carbon support a similar ordering semantic for using the discriminator values?

For comparison, reordering other declarations (e.g., fn, var) I don't perceive as changing the value in an equivalent way.

Also, depending on how much you try sharing syntax, while choice represents tagged unions, you might also want to consider how you might alter syntax for unnamed untagged unions (if they're even supported, e.g. for migrating multiple C++ unnamed unions on a single class type).

geoffromer commented 10 months ago
class Optional(T:! type) {
  choice None;
  choice Some(t: T);
}

If we go that route, it might be nice to take it one step further, and have an evolution path where you can customize the representation while leaving the choice syntax more or less in place, so that the reader can understand the API of a sum type without having to care whether it has a custom implementation. For example, maybe you could treat the choice declarations as static function/variable declarations, so that you could customize that definition of Optional like so:

class Optional(T:! type) {
  choice None = { .has_value = false };
  choice Some(t: T) { return {.has_value = true, .value = t }; }

  private var has_value: bool;
  private var value: T;

  impl as Match { ... }
}

That's not great, but it's a gesture toward the kind of thing I mean.

If not for that consideration, I would much prefer your other suggested syntax:

class Optional(T:! type) {
  choice {
    Some(t: T),
    None
  }
}

That keeps all the alternatives grouped together, which I think will be helpful for readability if the definition can contain other things like member functions. But maybe we can get the best of both, somehow?

Would Carbon support a similar ordering semantic for using the discriminator values?

We don't have a design for making the discriminator visible in the API (short of doing a full custom implementation) but we'll likely want that at some point, for interop/migration purposes. However, I think it should require the type to explicitly define the discriminator values, rather than have them be inferred based on the order.

Also, depending on how much you try sharing syntax, while choice represents tagged unions, you might also want to consider how you might alter syntax for unnamed untagged unions (if they're even supported, e.g. for migrating multiple C++ unnamed unions on a single class type).

I think we'll very likely want untagged unions, but trying to share syntax between them seems like it will impose a lot of cost for little benefit. The problems they need to solve don't have much overlap.

github-actions[bot] commented 7 months ago

We triage inactive PRs and issues in order to make it easier to find active work. If this issue should remain active or becomes active again, please comment or remove the inactive label. The long term label can also be added for issues which are expected to take time.

This issue is labeled inactive because the last activity was over 90 days ago.