-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathgenerics.txt
476 lines (309 loc) · 19.5 KB
/
generics.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
====== PHP RFC: Generic Types and Functions ======
* Version: 0.4.0
* Date: 2016-01-06
* Author: Ben Scholzen 'DASPRiD' <[email protected]>, Rasmus Schultz <[email protected]>
* Status: Draft
* First Published at: http://wiki.php.net/rfc/generics
**NOTE:** a newer version of this RFC may be under development [[https://github.com/mindplay-dk/php-generics-rfc|on GitHub]].
===== Introduction =====
This RFC proposes the addition of generic types and functions to PHP.
Generics enable developers to create a whole family of declarations using a single generic declaration - for example, a parameterized collection-type ''Collection<T>'' induces a declaration for a type-parameter ''T'', which negates the need to implement a dedicated collection-type for every entity-type, reducing the need for boilerplate code and duplication.
Generic type relationships are already possible in PHP, as it is in most dynamic languages - but such type relationships presently cannot be declared or checked, and, at this time, can't even be documented, e.g. using php-doc tags.
Generics are an important step towards PHP maturing as a gradually-typed language - a direction in which PHP is already moving, with the addition of scalar type-hints, and return type-hints, in PHP 7. Generics will provide stronger type-safety, when required, may enable new run-time optimizations, and substantial benefits in terms of static analysis in offline source code QA tools, as well as design-time inspections and auto-completion in modern IDEs.
Note that generic versions of the standard (SPL) collection types in PHP is currently beyond the scope of this RFC.
Also note that, while the syntax proposed by this specification may be similar to that of Hack/HHVM, compatibility with Hack is not a stated objective.
===== Terminology =====
This RFC uses the following terminology.
In a declaration such as ''class Dictionary<Key, Value> {}'', we refer to the type ''Dictionary'' as a "parameterized type", and to ''Key'' and ''Value'' as "type parameters".
In a declaration such as ''$d = new Dictionary<string, int>()'', we refer to ''string'' and ''int'' as "type arguments", and to the type of ''$d'' as the "reified type".
In a declaration such as ''function foo(A $a): B {}'', we refer to ''A'' and ''B'' as "type declarations".
===== Proposal =====
This RFC proposes the addition of generic classes, interfaces and traits - all of which will be referred to as generic "types" in the following. It also proposes the addition of generic functions and methods.
The proposed syntax and feature set references various generic features of gradually-typed languages such as Dart and TypeScript, as well as statically-typed languages like C# and Java, in an attempt to create something that looks and feels familiar to those with generics experience from other languages.
==== Parameterized Types ====
A parameterized type (class/trait/interface) declaration includes one or more type parameters, enclosed in angle brackets, immediately following the type-name.
A type parameter may optionally include an upper bound, e.g. a supertype of permitted type arguments for a given type parameter, which may be indicated by the use of the word ''is'' and a type-hint following the alias.
A type parameter may include a default type-argument, indicated by an ''='' sign folled by a valid, default type-declaration. The default type-argument is used in the absence of a type argument - it is reified and reflected, same as a given type-argument.
Within the scope of a parameterized type declaration, a type parameter may be used in a type-declarations, in any instance method declaration or expression body - this includes instance method return-types, and various common expressions in the body of an instance method, including the ''new'' statement, the ''::class'' constant, the ''instanceof'' operator, and references to static members.
Referencing a type-parameter in a static method declaration (or static method body) is not permitted, since there is no instance context (''$this'') and therefore no reified type arguments. (Static methods may declare their own type parameters, the same as type parameters in other function-declarations.)
The following demonstrates the proposed syntax for a parameterized class:
<code php>
class Entry<KeyType, ValueType>
{
protected $key;
protected $value;
public function __construct(KeyType $key, ValueType $value)
{
$this->key = $key;
$this->value = $value;
}
public function getKey(): KeyType
{
return $this->key;
}
public function getValue(): ValueType
{
return $this->value;
}
}
</code>
The class declaration ''Entry<KeyType, ValueType>'' in this example includes two type parameters, ''KeyType'' and ''ValueType''.
Note the use of type parameters being referenced in the type-declarations of the constructor arguments, as well as the return-type declarations of the ''getKey()'' and ''getValue()'' methods - the run-time type-checks performed will vary with the reified type-arguments.
An instance of a generic class can be constructed using explicit type arguments:
<code php>
$entry = new Entry<int,string>(1, 'test');
</code>
The type arguments, in this example, may also be inferred from the given arguments, rather than explicitly given:
<code php>
$entry = new Entry(1, 'test');
</code>
In this case, the type arguments are inferred and reified prior to execution of the constructor body. (Note that inference from ''null'' values is not permitted.)
Note that type arguments are internally reified, and can be accessed via reflection, but are not a part of the type-name:
<code php>
var_dump(get_class($entry)); // => (string) "Entry"
</code>
Also note that this means you can't have both a class ''C'' and a class ''C<T>'' in the same namespace - unlike some statically-linked languages, the type parameters aren't actually part of the class-name.
=== Extending a Parameterized Type ===
A type that extends a parameterized type may provide type argument declarations:
<code php>
class StringEntry extends Entry<int,string>
{}
</code>
The resulting class in this example is not parameterized, although it extends a parameterized class.
=== Type-checking of Parameterized Types ===
Function/method-declarations may include parameterized type-declarations - for example:
<code php>
function write(Entry<int, string> $entry)
{
// ...
}
write(new Entry<int, string>(1, 'test'));
write(new Entry<int, int>(1, 2)); // throws a TypeError
</code>
In the second example, the ''ValueType'' argument is ''int'', which is incompatible with the ''string'' type-argument used in the function declaration, and results in a ''TypeError''.
=== Nested Type Arguments ===
Generic classes may be instantiated and generic functions/methods may be called with nested type arguments.
<code php>
class Container<ContentType>
{
private $content;
public function getContent(): ContentType
{
return $this->content;
}
public function setContent(ContentType $content): void
{
$this->content = $content;
}
}
$container = new Container<Entry<int,string>>();
$container->setContent(new Entry<int,string>(1, 'test'));
var_dump($container->getContent() instanceof Entry<int,string>); // => (bool) true
$container->setContent(new Entry<int,int>(1, 1)); // throws a TypeError
</code>
In this example, the ''ContentType'' has retained the nested type arguments within ''Entry<KeyType, ValueType>''. The responsibility of type checking arguments to ''Entry'' still belong to the ''Entry'' class, yet the type hierarchy is maintained all the way up to the root definition ''Container<Entry<int,string>>''.
In the second example, the ''ValueType'' is incorrect just as before, which again results in a ''TypeError''.
=== Upper Bounds ===
The kind of type argument permitted for a given type parameter may be constrained, by using the ''is'' keyword to specify an upper bound for the type - for example:
<code php>
interface Boxable
{
// ...
}
class Box<T is Boxable>
{
// ...
}
class Hat implements Boxable
{
// ...
}
$box = new Box<Hat>(); // ok
$box = new Box<string>(); // throws a TypeError
</code>
In the second example, a ''TypeError'' is thrown, because ''string'' violates the upper bound ''T is Boxable''.
Any valid type-declaration may be used as an upper bound, including built-in types like ''int'', ''float'', ''bool'', ''string'' and ''object''. (Omission of an upper bound effectively means ''mixed'' in general PHP terms, though we are not proposing the ability to explicitly type-hint as ''mixed'', which isn't supported by PHP.)
Note that the choice of the keyword ''is'' to indicate upper bounds is based on the rejection of perhaps more obvious alternatives - repurposing the ''extends'' or ''implements'' keywords would be misleading, since they would work precisely the same way; worse, permitting both keywords would render consumer code invalid if an upper bound type provided by a library is refactored between class and interface. Repurposing ''instanceof'' would also be misleading, since the upper bound is checking the type-hint, not an instance. Furthermore, we don't want this to collide with possible future mixed scalar types, such as ''number'' or ''scalar'', neither of which make sense in conjunction with either ''extends'' or ''implements''. (If a reserved ''is'' keyword is undesirable for other reasons, a simple '':'' is likely a better alternative than overloading the meaning of an existing keyword.)
== Bounds Checking ==
Checking of upper bounds is consistent with type-checking in PHP - for example:
<code php>
interface Machine {}
class Computer implements Machine {}
class SuperComputer extends Computer {}
class MachineBuilder<T is Machine> {}
class ComputerBuilder<T is Computer> extends MachineBuilder<Computer> {} // (A)
class SuperComputerBuilder extends ComputerBuilder<SuperComputer> {} // (B)
</code>
In example (A) the type-argument ''Computer'' in ''MachineBuilder<Computer>'' is checked against the ''is Machine'' bound in the parameterized parent class ''MachineBuilder<T>'', and is found to be valid, because ''Computer'' implements ''Machine''.
In example (B) the type-argument ''SuperComputer'' in ''ComputerBuilder<SuperComputer>'' is checked against the ''is Computer'' bound in the parameterized parent class ''ComputerBuilder<T>'', and is found to be valid, because ''SuperComputer'' extends ''Computer''.
In other words, bounds are checked according to the same type-checking rules as objects being checked with ''instanceof'' - though it is important to note that upper bound checks are performed at the time when the declaration is loaded/parsed, ahead of any instance existing.
=== Traits ===
Traits can have type parameters:
<PHP>trait OuterIteratorDecorator<K, V> {
abstract function getInnerIterator(): Iterator<K,V>;
function rewind(): void {
$this->getInnerIterator()->rewind();
}
function valid(): bool {
return (bool) $this->getInnerIterator()->valid();
}
function key(): K {
return $this->getInnerIterator()->key();
}
function current(): V {
return $this->getInnerIterator()->current();
}
function next(): void {
$this->getInnerIterator()->next();
}
}
class MappingIterator<K, From, To> implements Iterator<K, From> {
use OuterIteratorDecorator<K, From>;
private $fn, $inner;
function __construct(callable $fn, Iterator<K, From> $it) {
$this->fn = $fn;
$this->inner = $it;
}
private function getInnerIterator(): Iterator<K, From> {
return $this->inner;
}
function current(): To {
return ($this->fn)($this->current());
}
}
</PHP>
Type arguments must be supplied via the ''use'' clause.
Note that a type argument passed to a trait may reference a type parameter declared by the parent class - for example:
<code php>
trait Container<T>
{
private $value;
public function get() : T
{
return $this->value;
}
public function set(T $value)
{
$this->value = $value;
}
}
class Box<T>
{
use Container<T>;
}
$box = new Box<Hat>();
$box->set(new Hat());
var_dump($box->get()); // => Hat#2
</code>
==== Parameterized Functions and Methods ====
Parameterized functions require a type-argument for the invokation itself - for example:
<code php>
declare(strict_types=1)
class Box<T>
{
public function __construct(T $content)
{
// ...
}
}
function create_box<T>(T $content): Box<T>
{
var_dump(func_type_args()); // => array("Hat")
return new Box<T>($content);
}
$box = create_box(new Hat());
$box = create_box<string>(new Hat()); // throws TypeError
</code>
In the first example, the type-argument ''T'' is inferred as ''Hat'', because the type parameter ''T'' was referenced in the type-declaration of the ''$content'' parameter.
The second example results in a ''TypeError'', because the type parameter ''T'' was explicitly defined as ''string''. (Note that, if we had not used ''declare(strict_types=1)'', and if ''Box'' had implemented ''<nowiki>__toString()</nowiki>'', this would have been acceptable, due to the default behavior of weak scalar type-checking.)
Note the addition of ''func_type_args()'', which returns a list of type-arguments pertaining to the current generic function call or constructor invocation. This complements ''func_get_args()'' by providing the list of type-arguments as fully-qualified class-names.
=== Parameterized Methods ===
Parameterized methods are subject to the same rules and behavior as parameterized functions, see above.
When overridden, parameterized methods must have type parameter declarations identical to those declared by the super-class - for example:
<code php>
interface Greetable
{
public function getName();
}
class Greeter
{
public function greet<T is Greetable>(T $subject)
{
$name = $subject->getName();
return "Hello, {$name}!";
}
}
class AdvancedGreeter extends Greeter
{
public function greet<T is Greetable>(T $subject)
{
return parent::greet($subject) . " - have a great day!";
}
}
</code>
Note that the type parameter ''T'' is the only thing that may change in a sub-class - the number and upper bounds must match.
The same applies when overriding constructors and static methods.
==== Generic Closures ====
TODO describe ''callable<T, ...>'' type-hints and/or parameterized ''Closure<T, ...>'' and/or ''Function<T, ...>'' types
==== Type Checking ====
Type-checks can be performed at run-time with the ''instanceof'' operator, either with or without type-arguments - for example:
<code php>
class Box<T> {}
class HeadGear {}
class Hat extends HeadGear {}
interface Feline {}
class Cat implements Feline {}
$hat_box = new Box<Hat>();
$cat_box = new Box<Cat>();
var_dump($hat_box instanceof Box); // => (bool) true
var_dump($cat_box instanceof Box); // => (bool) true
var_dump($cat_box instanceof Box<Cat>); // => (bool) true
var_dump($cat_box instanceof Box<Hat>); // => (bool) false
var_dump($cat_box instanceof Box<Feline>); // => (bool) true
var_dump($hat_box instanceof Box<HeadGear>); // => (bool) true
</code>
Note that type-checks against a parameterized type with no type-arguments (and no defaults) will check the base type only - any reified types will not be checked.
As demonstrated by the last two examples, type-checking also works on abstract types - that is, an instance of ''Box<Cat>'' is an instance of ''Box<Feline>'' because ''Cat'' implements ''Feline''; and likewise, ''Box<Hat>'' is an instance of ''Box<HeadGear>'' because ''Hat'' extends ''HeadGear''. (Consistent with the inability to type-check against traits, trait-names cannot be used as part of a type-check.)
=== Bounded Polymorphism ===
TODO: decide whether or not [[https://en.wikipedia.org/wiki/Bounded_quantification|bounded polymorphism]] should be supported.
=== Multiple Constraints ===
TODO: decide whether or not multiple constraints should be supported, e.g. with a Java-like syntax:
<code php>
class A<T> where T is T1, T is T2 {
// ...
}
</code>
This may relate to the [[https://wiki.php.net/rfc/union_types|union types RFC]] - if implemented, it may be more natural to expect support for union types as bounds.
==== Autoloading ====
When autoloading is triggered e.g. by a ''new'' statement with a parameterized type, autoloading is triggered as normal, with only the class-name (without type parameters) being supplied.
In other words, a statement like ''new Map<int,string>()'' will trigger auto-loading of class ''Map''.
==== Reflection ====
Type parameters, as well as reified type arguments, are available via reflection.
This RFC calls for the following changes and additions to the reflection API:
TODO (some [[https://gist.github.com/mindplay-dk/dc3d24eba8d13a650cc6|notes]] with ideas are available.)
=== Reification ===
Because PHP is a reflective language, type arguments are fully [[https://en.wikipedia.org/wiki/Reification_(computer_science)#Reification_and_reflective_programming_languages|reified]] - which means they actually exist (in memory) at run-time.
This differs from generics in Hack, where type-hints are [[https://github.com/facebook/hhvm/issues/5317|not reified]] and are unavailable at run-time. Type erasure would be inconsistent with the existing PHP type system, where any type-declaration is generally available at run-time via reflection.
==== Related Enhancements ====
The following enhancements are somewhat beyond the scope of generics, but should be considered as part of this RFC.
== Static Type-checking With ''instanceof'' ==
Consider enhancing the ''instanceof'' keyword, which does not currently work for any of the scalar types - for example, ''123 instanceof int'' currently returns ''false''.
Also consider the addition of ''object'' as a keyword - currently, ''new Foo() instanceof object'' returns ''false''.
These enhancements would seem natural and consistent with the addition of scalar type-hints in PHP 7, and implementation of the actual type-checks, are necessary under any circumstances, in order for ''instanceof'' to work properly in conjunction with scalar type arguments, and also are required for upper bound checks.
== New Pseudo-types ==
Consider the introduction of a new pseudo-type ''scalar'' as a super-type of ''int'', ''float'', ''string'', ''bool'' and ''float'', as well as the introduction of a pseudo-type ''number'' as a super-type of ''int'' and ''float''.
Introduction of these types would allow better use of the upper bounds feature, e.g. allowing one type to specify an upper bound of ''scalar'', and a sub-type to specify the kind of scalar type.
===== Backward Incompatible Changes =====
No BC breaks are expected from this proposal.
===== Proposed PHP Version(s) =====
This proposal aims for PHP 7.1.
===== Proposed Voting Choices =====
For this proposal to be accepted, a 2/3 majority is required.
===== Patches and Tests =====
No patch has been written for this yet. As I'm not a C-coder myself, I encourage others to write a patch based on this proposal.
Some [[https://github.com/orolyn/php-src/tree/generics-tests/Zend/tests/generics|preliminary tests]] have been written for most key concepts and behaviors. Most notably, at this time, tests for reflection API enhancements are still missing.
The same fork also contains some experimental parser enhancements written by Dominic Grostate.
===== Related RFCs =====
Generic arrays (and related functions) were previously part of this RFC, but have been moved to a dedicated [[https://wiki.php.net/rfc/generic-arrays|Generic arrays RFC]].
===== References =====
https://en.wikipedia.org/wiki/Generic_programming