Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lists of different kinds of values

A list has one element type, but that type can describe alternatives. Use a wrapper enum when the alternatives form a concept in your program and share operations. Use an anonymous sum when a local choice of whole types is enough.

Give the alternatives a shared interface

A message and a status can both appear in a feed. FeedItem names that concept and keeps the dispatch in its label method:

class Message:
    text: str

    def label(self) -> str:
        self.text

enum Status:
    Ready
    Busy

    def label(self) -> str:
        match self:
            case Status.Ready:
                "ready"
            case Status.Busy:
                "busy"

enum FeedItem:
    Message(Message)
    Status(Status)

    def label(self) -> str:
        match self:
            case FeedItem.Message(message):
                message.label()
            case FeedItem.Status(status):
                status.label()

def main() -> Result[(), AllocError | IoError]:
    mut items: list[FeedItem] = []?
    items.append(FeedItem.Message(Message("hello")))?
    items.append(FeedItem.Status(Status.Ready))?
    for item in &items:
        print(item.label())?
    Ok(())
hello
ready

The wrapper can carry class instances, enum values, or other owned data. Constructing a variant moves an affine payload into it; appending moves that wrapper into the list. The loop borrows the list, and matching self borrows the active payload, so neither operation needs a copy.

Each kind has its own implementation of label, but the wrapper’s method is what lets the loop call one interface. No protocol declaration is necessary. Adding a variant makes the compiler check that the wrapper’s match handles it.

Use an anonymous sum for a local choice

If you do not need a named wrapper or its methods, declare the alternatives in the list’s element type. The annotation lets append accept either member directly:

class Message:
    text: str

enum Status:
    Ready
    Busy

def main() -> Result[(), AllocError | IoError]:
    mut items: list[Message | Status] = []?
    items.append(Message("hello"))?
    items.append(Status.Ready)?
    for item in &items:
        match item:
            case message: Message:
                print(message.text)?
            case status: Status:
                match status:
                    case Status.Ready:
                        print("ready")?
                    case Status.Busy:
                        print("busy")?
    Ok(())
hello
ready

case message: Message: selects a whole member type. Because item is a borrow, its matched payload is borrowed too. The Status member contains any of that enum’s variants, so its own match handles both.

The sum does not automatically forward methods, even if every member has a method with the same signature. Match to select a member, or choose the wrapper pattern when several callers need the same operation.

Both forms store a tag and payload inline; wrapping adds no heap allocation. List creation and growth still return allocation results, as does constructing any payload that itself needs storage. See anonymous sums and enums for the full contracts.

For implementations supplied independently of a fixed list of alternatives, see protocol objects. Each object uses an explicit heap allocation and exposes the methods of one protocol.