Skip to content

Commit

Permalink
Reflect key ideas from p0157 in design docs.
Browse files Browse the repository at this point in the history
Notable changes from p0157:
- Syntax and semantics are updated to reflect subsequent design work, especially on variable declarations, generics, and classes.
- `Matchable.Match` is now `Match.Op`, following the resolution of carbon-language#1058.
  • Loading branch information
geoffromer committed Sep 15, 2022
1 parent c25a2b2 commit d87173d
Show file tree
Hide file tree
Showing 2 changed files with 198 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/design/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2048,6 +2048,7 @@ choice LikeABoolean { False, True }

> References:
>
> - [Sum types](sum_types.md)
> - Proposal
> [#157: Design direction for sum types](https://github.com/carbon-language/carbon-lang/pull/157)
> - Proposal
Expand Down
197 changes: 197 additions & 0 deletions docs/design/sum_types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# Sum types

<!--
Part of the Carbon Language project, under the Apache License v2.0 with LLVM
Exceptions. See /LICENSE for license information.
SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-->

<!-- toc -->

## Table of contents

- [Overview](#overview)
- [`choice` declarations](#choice-declarations)
- [User-defined sum types](#user-defined-sum-types)
- [Alternatives considered](#alternatives-considered)
- [References](#references)

<!-- tocstop -->

## Overview

In Carbon, a _sum type_ is a type whose values are grouped into several distinct
named cases, called _alternatives_. A value of a sum type logically consists of
a value of one of the alternatives, together with a _discriminator_ tag, which
identifies which alternative is present. Sum types are typically handled with
pattern matching.

## `choice` declarations

The `choice` keyword is used to declare a sum type by specifying its interface,
leaving the implementation to the compiler. For example:

```carbon
choice OptionalI32 {
Some(value: i32),
None
}
```

This declares a sum type named `OptionalI32` with two alternatives: `Some`,
which holds a single `i32` value, and `None`, which is empty. Choice types can
also be parameterized, like class types:

```carbon
choice Optional(T:! Type) {
Some(value: T),
None
}
```

An alternative declaration consists of the alternative name, followed by an
optional parameter list in parentheses, much like the syntax of a function
declaration. A value of a function-like alternative is specified by "calling" it
like a function, and a value of a "bare" alternative like `None` is specified by
naming it:

```carbon
var my_opt: Optional(i32) = Optional(i32).None;
my_opt = Optional(i32).Some(42);
```

The value of a choice type can be inspected using a `match` statement:

```carbon
match (my_opt) {
case .Some(the_value: i32) => {
Print(the_value);
}
case .None => {
Print("None");
}
}
```

## User-defined sum types

`choice` declarations are a convenience shorthand for common use cases, but they
have limited flexibility. There is no way to control the representation of a
`choice` type, or define methods or other members for it (although you can
extend it to implement interfaces, using
[`external impl`](generics/overview.md#implementing-interfaces) or
[`adapter`](generics/overview.md#adapting-types)). However, a `class` type can
be extended to behave like a sum type. This is much more verbose than a `choice`
declaration, but gives the author full control over the representation and class
members.

The ability to create instances of the sum type can be straightforwardly
emulated with factory functions and static constants (although the latter
[are not yet designed](classes.md#no-static-variables)), and the internal
storage layout will presumably involve untagged unions or some other low-level
storage primitive which hasn't been designed yet, but the key to defining a sum
type's interface is enabling it to support pattern matching. To do that, the sum
type has to specify two things:

- The set of all possible alternatives, including their names and parameter
types, so that the compiler can typecheck the `match` body, identify any
unreachable `case`s, and determine whether any `case`s are missing.
- The algorithm that, given a value of the sum type, determines which
alternative is present, and specifies the values of its parameters.

Here's how that would look if `Optional` were defined as a class:

```carbon
class Optional(T:! Type) {
// Factory functions
fn Some(value: T) -> Self;
fn None() -> Self;
private var has_value: bool;
private var value: T;
interface MatchContinuation {
let template ReturnType:! Type;
fn Some(value: T) -> ReturnType;
fn None() -> ReturnType;
}
external impl as Match(MatchContinuation) {
fn Op[me: Self, Continuation:! MatchContinuation](
continuation: Continuation*) {
if (me.has_value) {
return continuation->Some(me.value);
} else {
return continuation->None();
}
}
}
// Operations like destruction, copying, assignment, and comparison are
// omitted for brevity.
}
```

In this code, `Optional` makes itself available for use in pattern matching by
declaring that it implements the `Match` interface. `Match` takes an interface
argument, called the _continuation interface_, which specifies the set of
possible alternatives by declaring a method for each one. In this case, we pass
`Optional.MatchContinuation` as the continuation interface.

When compiling a `match` statement, the compiler checks that the type being
matched implements `Match(C)` for some continuation interface `C`. Then, it
notionally transforms the `match` body into a class that implements `C`, with
one method for each `case`, and then passes that to `Match.Op` on the object
being matched. For example, the `match` statement shown earlier might be
transformed into:

```carbon
class __MatchStatementImpl {
fn Make() -> Self { return {}; }
impl as Match(Optional.MatchContinuation) where .ReturnType = () {
fn Some(the_value: i32) {
Print(the_value);
}
fn None() {
Print("None");
}
}
}
my_opt.(Match.Op)(__MatchStatementImpl.Make());
```

Thus, a `match` statement works by invoking the sum type's `Match.Op` method,
which is responsible for determining which alternative the sum object
represents, and then invoking the compiler-supplied continuation that
corresponds to that alternative. In order for this scheme to work, `Match.Op` is
required to invoke exactly one method of `MatchContinuation`, and to do so
exactly once.

Notice that the names `Some` and `None` are defined twice, once as factory
functions of `Optional` and once as methods of `MatchContinuation`, with the
same parameter types in each case. The two effectively act as inverses of each
other: the factory functions compute an `Optional` from their parameters, and
the methods are used to report the parameter values that would compute a given
`Optional`. This mirroring between expression and pattern syntax is ultimately a
design choice by the type author; there is no language-level requirement that
the alternatives correspond to the factory functions, but it is **strongly**
recommended.

## Alternatives considered

- [Providing `choice` types only](/proposals/p0157.md#choice-types-only), with
no support for user-defined sum types.
- [Indexing alternatives by type](/proposals/p0157.md#indexing-by-type)
instead of by name.
- Implementing user-defined sum types in terms of
[`choice` type proxies](/proposals/p0157.md#pattern-matching-proxies) rather
than callbacks.
- Implementing user-defined sum types in terms of invertible
[pattern functions](/proposals/p0157.md#pattern-functions).

## References

- Proposal
[#157: Design direction for sum types](https://github.com/carbon-language/carbon-lang/pull/157)

0 comments on commit d87173d

Please sign in to comment.