Skip to content

Commit 08a5376

Browse files
viratyosinmeta-codesync[bot]
authored andcommitted
Add case type docs to static docs
Summary: User-facing docs for the case types feature Intentionally not covered: * recursive case types * conditional case types Reviewed By: madgen Differential Revision: D89055262 fbshipit-source-id: 408bba1dfac4470adf4f24b0219907e54ac1173a
1 parent 236b46d commit 08a5376

1 file changed

Lines changed: 151 additions & 0 deletions

File tree

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
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

Comments
 (0)