-
Notifications
You must be signed in to change notification settings - Fork 213
[Inline Classes] Phantom Type support? #2865
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
I'd say "yes", it you can live with the phantom types only existing at compile-time. inline class ListContent { final Never _; }
inline class Empty implements ListContent { final Never _; }
inline class NonEmpty implements ListContent { final Never _; }
inline class One implements NonEmpty { final Never _; }
inline class Many implements NonEmpty { final Never _; } and inline class List2<T, Restriction extends ListContent> {
final List<T> elements;
static List2<T, Empty> empty<T>() => <T>[] as List2<T, Empty>;
static List2<T, One> single<T>(T value => <T>[] as List2<T, One>;
static List2<T, Many> all<T>(Iterable<T> elements) {
var values = elements.toList();
if (values.length < 2) throw ArgumentError("Not many elements", "elements");
return values as List2<T, Many>();
}
} If you want the phantom types to be reified at runtime, so you can do |
How will this be enforced? (i.e. the inline class List2<T, Unrestricted /* can be anything? */> {
List2<T> list;
} In case my description was a little unclear, I'm mostly interesting in using inline classes to remove the overhead of the wrapper and support unrestricted phantom types at the same type (not necessarily in getting rid of any costs that are intrinsic to phantom types, although that would be nice) |
This should just work. However, the implementation of inline classes doesn't yet handle all kinds of type inference (so we need to write a couple of extra type arguments explicitly until that has been fixed). I don't see any reason to make the phantom type hierarchy an inline class hierarchy (it's a fancy idea, but I don't think it brings any benefits ;-). The effect would simply be that we reserve the same amount of space to store the value of a type variable, but that type variable would always have the value So here we go ( // ----------------------------------------------------------------------
// Lists with phantom classification.
abstract final class ListContents {}
abstract final class Empty implements ListContents {}
abstract final class NonEmpty implements ListContents {}
abstract final class One implements NonEmpty {}
abstract final class Many implements NonEmpty {}
inline class ListPhantom<T, State extends ListContents> {
final List<T> elements;
ListPhantom._(this.elements);
ListPhantom<T, NonEmpty>? get asNonEmptyOrNull =>
<State>[] is List<NonEmpty> ? this as dynamic : null;
static ListPhantom<T, Empty> empty<T>() =>
ListPhantom<T, Empty>._([]);
static ListPhantom<T, One> single<T>(T value) =>
ListPhantom<T, One>._([value]);
static ListPhantom<T, Many> all<T>(Iterable<T> elements) {
var values = elements.toList();
if (values.length < 2) {
throw ArgumentError("Not many elements", "elements");
}
return ListPhantom<T, Many>._(values);
}
}
// ----------------------------------------------------------------------
// Example usage.
void main() {
final a = ListPhantom.empty<int>();
final b = ListPhantom.single(0);
final c = ListPhantom.all([1, 2]);
// takeOnlyNonemptyLists(a); // Compile-time error.
takeOnlyNonemptyLists(b); // OK.
takeOnlyNonemptyLists(c); // OK.
void f<T, State extends ListContents>(ListPhantom<T, State> list) {
var maybeList = list.asNonEmptyOrNull;
if (maybeList != null) takeOnlyNonemptyLists(maybeList);
}
// Type arguments will be inferred (when that has been fixed).
f<int, Empty>(a); // No output.
f<int, One>(b); // Prints '0'.
f<int, Many>(c); // Prints '0'.
}
void takeOnlyNonemptyLists<T>(ListPhantom<T, NonEmpty> list) {
print(list.elements.first);
} The most interesting part is probably the treatment of phantom type arguments at run time: We can't communicate to the type system that a given type variable has a value which is a subtype of anything specific (here: |
Thank you for the hint that the experiment is ready to be tested. There are some other things related to phantom types that I need to try, I will report any issues that I encounter. (I've tried to hide the inline class behind an interface which did not work for me: dart-lang/sdk#51564) |
That's inherently impossible: The word 'inline' indicates that there is no wrapper object, so there's no representation of abstract class Foo { void bar(); }
inline class I1 implements Foo { // Let's just pretend that we can implement `Foo`.
final int i;
I1(this.i);
void bar() => print('1-bar');
}
inline class I2 implements Foo { // Ditto.
final int i;
I2(this.i);
void bar() => print('2-bar');
}
bool b = true;
void main() {
Foo = b ? I1(42) : I2(42);
b.bar(); // `identical(b, 42)` is true, and so is `b is int`. There's no trace of the inline classes.
} We can't distinguish We can actually allow |
Anyway, it looks like there are no actionable issues here, so I'll close the issue. @modulovalue, please open new ones as needed if anything is still unresolved. |
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:
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:
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.)
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?
The text was updated successfully, but these errors were encountered: