Skip to content

[Inline Classes] Phantom Type support? #2865

Closed
@modulovalue

Description

@modulovalue

This issue is a question about whether the Inline Classes proposal is able to support Phantom Type use cases.

I will first introduce the idea of Phantom Types in the context of Dart and provide a small motivating example for them.

Phantom Types

Consider the following:

abstract class Foo<T> {}

The type parameter T is unused. I will refer to type parameters that are not used anywhere inside of the body of the associated declaration as Phantom Types.

State model

Consider the following interface hierarchy:

abstract class Listcontent {}

abstract class Empty implements Listcontent {}

abstract class Nonempty implements Listcontent {}

abstract class One implements Nonempty {}

abstract class Many implements Nonempty {}

This models the state of the contents of a List i.e. a list can be empty, it can have one element or many. If it has one or many elements, it is considered to be nonempty.

I will use of the Listcontent hierarchy below.

Proving and carrying around facts about the contents of a List

Here is an example of proving, encoding and carrying around a proof that a list is not empty using Phantom Types and the hierarchy from above. (Note: This is a simplified example that is meant to present the key idea and does not represent a safe implementation.)

/// A wrapper for Dart Lists that is able to carry a Phantom Type P.
class List2<T, P> {
  final List<T> list;

  const List2(this.list);
}

/// A function that maps a list to either Null or to a
/// list that is annotated to be not empty.
List2<T, Nonempty>? prove_list_nonempty<T>(
  final List<T> list,
) {
  if (list.length == 0) {
    return null;
  } else if (list.length == 1) {
    return List2<T, One>(list);
  } else {
    return List2<T, Many>(list);
  }
}

void main() {
  final a = prove_list_nonempty([]);
  final b = prove_list_nonempty([0]);
  final c = prove_list_nonempty([0, 1]);
  if (a != null) {
    // Prints nothing. This branch is never reached
    // because a is null because [] is empty.
    take_only_nonempty_lists(a);
  }
  if (b != null) {
    // Prints something because [0] has one element which makes it not empty.
    take_only_nonempty_lists(b);
  }
  if (c != null) {
    // Prints something because [0, 1] has many elements which makes is not empty.
    take_only_nonempty_lists(c);
  }
}

// An example of a function that depends on a list that is not empty.
void take_only_nonempty_lists<T>(
  final List2<T, Nonempty> list,
) {
  // We know that 'list' is not empty.
  // We could make use of this fact and e.g. map the list
  // to a new list while maintaining the proof that the 
  // new mapped list is also not empty.
  print(list.list);
}

Notice how take_only_nonempty_lists expects us to give it a nonempty List. Its requirements have been encoded as a Type. This allows us to provide greater safety guarantees.

I hope this shows that Phantom Types are a useful idea as they allow us to maintain properties through Types. Without them, in the example above, we'd have to write down our expectations as comments and hope that no user violates any of our expectations. Or alternatively, we would have to re-check whether our assumptions about any given list are true which is not free (and can be expensive for some properties such as maintaining whether a list is sorted.).

Inline Classes support for Phantom Types

If we want to use phantom types to add more safety to our programs today, we have to pay for a wrapper type. We are forced to make a trade-off between performance and safety. I'm very excited about inline classes because I hope that they could make annotating values with Phantom Types cheaper i.e. remove the costs of the wrapper type (List2 in the example above).

Would the inline classes proposal allow for wrapper types to be zero-cost (i.e. the wrapper object would be free, we'd have to only pay for the phantom type and the wrapped value) or would that not be possible because List2 declares a type parameter that is not being used in the wrapped value?

Metadata

Metadata

Assignees

No one assigned

    Labels

    requestRequests to resolve a particular developer problem

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions