|
| 1 | +# Case Types |
| 2 | + |
| 3 | +Case types are a special kind of type alias that enable declaration of runtime-disjoint unions. |
| 4 | + |
| 5 | +## Basic Syntax |
| 6 | + |
| 7 | +```hack |
| 8 | +case type Name = Variant1 | Variant2 | ... ; |
| 9 | +case type Name<T1, T2> as UpperBound = Variant1 | Variant2 | ... ; |
| 10 | +``` |
| 11 | + |
| 12 | +**Key Components:** |
| 13 | +- **Name** The identifier for the case type |
| 14 | +- **Type parameters** (optional) |
| 15 | +- **Upper bound** (optional) Declared with `as`, implicitly `mixed` if not specified |
| 16 | +- **Variants** The component type(s), separated by `|` |
| 17 | + |
| 18 | +### Examples |
| 19 | + |
| 20 | +```hack |
| 21 | +case type SimpleCaseType = int | MyClass; |
| 22 | +
|
| 23 | +case type BoundedAndGenericCaseType<+Tk as arraykey> as nonnull = |
| 24 | + keyset<Tk> | int; |
| 25 | +``` |
| 26 | + |
| 27 | +## The Runtime-Disjoint Requirement |
| 28 | + |
| 29 | +The variants of a case type must be known to be shallowly disjoint at runtime. |
| 30 | +This is defined by mapping each type to a set of runtime data tags. |
| 31 | +Two types are runtime-disjoint if they do not share any runtime tags. |
| 32 | + |
| 33 | +The purpose of this requirement is to enable runtime-efficient, exhaustive decomposition of the type that fully recovers type information. |
| 34 | + |
| 35 | +**Example of runtime data tags:** |
| 36 | +- Primitive tags: `int`, `bool`, `float`, `string`, `null` |
| 37 | +- Array tags: `vec`, `dict`, `keyset` |
| 38 | + - Note that shapes and tuples are dicts and vecs respectively at runtime and so do not have their own distinct tag. |
| 39 | + - Note that array types' generics are erased and so do not affect the tag. |
| 40 | +- Object tags: There is a tag for each class. |
| 41 | + - Note that only reified generics are included in the tag. |
| 42 | + - Note that disjointness accounts for inheritance. For example, two unsealed interface types are not considered disjoint because a single class could implement both. But two abstract classes are disjoint. |
| 43 | + |
| 44 | +### Valid (Disjoint) Examples |
| 45 | + |
| 46 | +```hack |
| 47 | +// Different primitives |
| 48 | +case type Good1 = int | string | bool; |
| 49 | +
|
| 50 | +// Different array kinds |
| 51 | +case type Good2 = vec<int> | dict<int, int>; |
| 52 | +
|
| 53 | +// vec vs shape |
| 54 | +case type Good3 = vec<int> | shape('x' => int); |
| 55 | +
|
| 56 | +// Final class vs interface - it is known that C does not implement I2 |
| 57 | +final class C {} |
| 58 | +interface I2<T> {} |
| 59 | +case type Good4 = C | I2<int>; |
| 60 | +
|
| 61 | +// due to the `as arraykey` bound, we know that `T` is disjoint from `float` |
| 62 | +case type GenericCaseType<T as arraykey> = T | float; |
| 63 | +``` |
| 64 | + |
| 65 | +### Invalid (Overlapping) Examples |
| 66 | + |
| 67 | +```hack |
| 68 | +// vec and Traversable overlap (vec implements Traversable) |
| 69 | +case type Bad1 = vec<int> | Traversable<string>; |
| 70 | +
|
| 71 | +// Two interfaces can overlap (a class could implement both) |
| 72 | +interface I1 {} |
| 73 | +interface I2 {} |
| 74 | +case type Bad2 = I1 | I2; |
| 75 | +
|
| 76 | +// vec and tuple have overlapping runtime representation |
| 77 | +case type Bad3 = vec<int> | (int, int); |
| 78 | +
|
| 79 | +// shape fields are not considered |
| 80 | +case type Bad4 = shape('x' => int) | shape('y' => string); |
| 81 | +
|
| 82 | +// E is a subtype of C, so they overlap |
| 83 | +class C {} |
| 84 | +class E extends C {} |
| 85 | +case type Bad5 = C | E; |
| 86 | +``` |
| 87 | + |
| 88 | +## Subtyping |
| 89 | + |
| 90 | +The treatment of case types differs when it appears on different sides of a subtyping check. |
| 91 | +i.e. "Is a value of type CaseType a value of type T?" (`CaseType <: T`) v.s. "Is a value of T a subtype of a value of type CaseType?" (`T <: CaseType`). |
| 92 | + |
| 93 | +The purpose of this asymmetry is to avoid quadratic performance costs related to subtyping union types. |
| 94 | + |
| 95 | +### When Case Type is on the Super Side (Right Side) |
| 96 | + |
| 97 | +When a case type appears on the **right side** of a subtype check (as a supertype), it is **expanded to its union**: |
| 98 | + |
| 99 | +```hack |
| 100 | +case type CT = int | string; |
| 101 | +
|
| 102 | +// When CT is expected (super side), we can pass int or string |
| 103 | +function accept_ct(CT $x): void {} |
| 104 | +
|
| 105 | +accept_ct(42); // OK: int <: (int | string) |
| 106 | +accept_ct("hello"); // OK: string <: (int | string) |
| 107 | +``` |
| 108 | + |
| 109 | +### When Case Type is on the Sub Side (Left Side) |
| 110 | + |
| 111 | +When a case type appears on the **left side** of a subtype check (as a subtype), its **upper bound is used** instead of the union: |
| 112 | + |
| 113 | +```hack |
| 114 | +case type CT_Bounded as arraykey = int; |
| 115 | +case type CT_No_Bounds = int; |
| 116 | +
|
| 117 | +function expect_int(int $x): void {} |
| 118 | +function expect_arraykey(arraykey $x): void {} |
| 119 | +function expect_mixed(mixed $x): void {} |
| 120 | +
|
| 121 | +function test(CT_Bounded $bounded, CT_No_Bounds $unbounded): void { |
| 122 | + expect_int($bounded); // ERROR: arraykey </: int |
| 123 | + expect_arraykey($bounded); // OK: arraykey <: arraykey |
| 124 | + expect_mixed($bounded); // OK: arraykey <: mixed |
| 125 | +
|
| 126 | + expect_int($unbounded); // ERROR: mixed </: int |
| 127 | + expect_arraykey($unbounded);// ERROR: mixed </: arraykey |
| 128 | + expect_mixed($unbounded); // OK: mixed <: mixed |
| 129 | +} |
| 130 | +``` |
| 131 | + |
| 132 | +## Decomposition |
| 133 | + |
| 134 | +Case types can be decomposed using `is` runtime type checks: |
| 135 | + |
| 136 | +```hack |
| 137 | +case type MyCaseType = int | string | MyClass; |
| 138 | +
|
| 139 | +function takes_case_type(MyCaseType $x): void { |
| 140 | + if ($x is int) { |
| 141 | + // $x : int |
| 142 | + } else { |
| 143 | + // $x : string | MyClass |
| 144 | + if ($x is string) { |
| 145 | + // $x : string |
| 146 | + } else { |
| 147 | + // $x : MyClass |
| 148 | + } |
| 149 | + } |
| 150 | +} |
| 151 | +``` |
0 commit comments