====== PHP RFC: Native Immutable Collections — "vec", "set" and "tuple" ====== * **Version:** 1.0 * **Date:** 2026-08-04 * **Author:** Michał Marcin Brzuchalski * **Status:** Draft * **Target version:** PHP 9.0 * **Implementation:** [[https://github.com/brzuchal/php-src/tree/first-class-collections|php-src branch ''first-class-collections'']] ===== Introduction ===== PHP's ''array'' is a single, mutable type that serves as an ordered list, an associative map, a record and a set at once. That versatility means an ''array'' type declaration conveys almost nothing: not the element type, not whether the value is a sequence or a map, not whether it is a fixed-arity record, and not whether it may be mutated after construction. This RFC proposes three **native, immutable, value-typed collections**: * **''vec[T]''** — an ordered, densely indexed sequence of ''T''; * **''set[T]''** — a collection of distinct ''T''; * **''tuple[A, B, …]''** — a fixed-arity, positionally typed record. $ids = vec[int]{1, 2, 3}; $perms = set[string]{"read", "write", "read"}; // {"read", "write"} $pair = tuple[int, string]{200, "OK"}; Each is a first-class runtime value with its own type tag — not an object and not an array with conventions. Each is **immutable** (length, order and contents are fixed at construction) and carries a **declared element or member type** that is validated at construction and is part of the value's runtime identity, so ''vec[int]{1}'' and ''vec[float]{1.0}'' are different types and a ''vec[int]'' is never interchangeable with a ''vec[float]''. ===== Motivation ===== * **An enforced element type.** ''vec[int]'' guarantees, at the language level, that every element is an ''int'' — checked by the engine, not by an external analyser. * **Distinct sequence, set and record contracts.** Order and density (''vec''), distinctness (''set'') and fixed positional arity (''tuple'') are three different contracts an ''array'' cannot distinguish. * **Value semantics and immutability.** A collection compares by contents (''===''), cannot be mutated in place, and can be shared without defensive copies. * **In the engine, because it must be.** PHP has no language-level generics (the bound-erased generics RFC was declined in June 2026, and reified generics are considered too costly), so an enforced element type cannot be delegated to a userland ''Collection''; it has to live in the engine. This is complementary to generics rather than a substitute: a future generics feature could sit alongside these types (a generic collection library might even build on them), and nothing here forecloses that design. ===== Proposal ===== This RFC adds three collection kinds — ''vec'', ''set'', ''tuple'' — as native immutable value types, with: * a literal syntax with an explicit element/member type; * their use as type declarations for parameters, return types and properties, including nested and nullable forms and the bare ''vec[]'' / ''set[]'' / ''tuple[]'' forms; * strict, coercion-free validation of every element at construction; * shallow immutability and value semantics (''==='', truthiness, conversions); * integer indexing (read-only) for ''vec'' and ''tuple'', by-value ''foreach'', ''iterable'' integration, and the intrinsic read-only ''->count'' / ''->isEmpty''; * immutable-update methods that return new collections; * support in ''serialize''/''unserialize'', ''var_export'', ''var_dump'', ''print_r'', ''debug_zval_dump'', ''gettype'', ''get_debug_type'', and Reflection. ''map'', ''shape'', functional/bulk methods, contextual literal inference, a general update expression, and a global ''count()'' overload are **not** part of this RFC; see Future scope. ===== Syntax ===== A collection type is written with a **compound head token** — the kind name immediately followed by ''['', with **no whitespace between them**: collection_type : head '[' type_list ']' collection_literal : head '[' type_list ']' '{' element_list? '}' head : 'vec' | 'set' | 'tuple' type_list : type (',' type)* (no trailing comma) element_list : expr (',' expr)* ','? (trailing comma allowed) * The element/member type list is **always explicit**: there is no type-argument-less literal — ''vec{1, 2, 3}'' is a syntax error. * The literal body is a plain comma-separated expression list, **not** an array-pair list: keys (''{0 => 1}''), by-reference elements (''{&$x}'') and spread (''{...$a}'') are syntax errors. * An empty body is allowed for ''vec'' and ''set'' (''vec[int]{}''); a ''tuple'' has at least one member. * The head must be adjacent to ''['': ''vec [int]'' (with a space) and ''vec/*c*/[int]'' are parse errors. **''vec'', ''set'' and ''tuple'' are not reserved words.** Only the adjacent two-token sequence ''name['' in type/expression position is special, so existing code keeps working: function vec(int $n): void {} // function named vec class set {} // class named set const tuple = 7; // constant named tuple $o->vec[0]; // property access then subscript Foo::vec[0]; // class-constant access then subscript ===== Type declarations ===== ''vec[T]'', ''set[T]'' and ''tuple[A, B, …]'' may be used anywhere an ordinary type may be used — parameters, return types, and properties (including promoted and ''readonly'' properties, and interface/abstract method signatures). Ordinary PHP visibility and ''readonly'' rules apply unchanged. function sum(vec[int] $values): int { /* … */ } function headers(): vec[string] { /* … */ } final class Response { public function __construct( public readonly tuple[int, string] $status, public readonly set[string] $varyHeaders, ) {} } ==== Concrete collection types ==== At the **declaration** level the member-type grammar is permissive: it accepts builtin types, class names and nested collections. Not every member type a declaration accepts can hold a //value//, however; that narrower question is covered under Construction and validation. Collections nest without special syntax, because a member type is an ordinary type: function matrix(): vec[vec[int]] { /* … */ } function pairs(): vec[tuple[int, string]] { /* … */ } ==== Bare collection types — ''vec[]'', ''set[]'', ''tuple[]'' ==== Empty brackets give a **bare** collection type meaning *any element type of that kind* — the top of that kind's hierarchy: function rows(vec[] $any): int { return $any->count; } // any vec accepted rows(vec[int]{1, 2}); // ok rows(vec[string]{"a"}); // ok rows(set[int]{1}); // TypeError: … must be of type vec[], set[int] given * Usable in **type positions only** (parameters, returns, properties, ''?vec[]''); there is **no bare literal** — ''vec[]{}'' is a parse error. * ''tuple[]'' also erases arity, accepting a tuple of any length. * ''vec[]'' accepts a vec of **any** element type — it is the top of the kind, the one supertype (a concrete ''vec[T]'' is invariant and accepts only ''vec[T]''). It is not "a vec of ''mixed''": ''mixed'' is not a constructible element type (Construction and validation), so there is no ''vec[mixed]'' //value// to confuse it with. ==== Nullability, nesting and variance ==== Nullability uses the ordinary prefix ''?'' form (''?vec[Row]''). A collection type **may not** be a member of a union or intersection (bare forms included); this is a compile-time error naming the fix: function f(vec[int]|null $x) {} // Fatal error: Collection type cannot be part of a union type; // write ?vec[...] for a nullable collection Concrete collection types are **invariant** in their members: ''vec[Dog]'' is not a ''vec[Animal]'' (nor the reverse), and ''vec[int]'' is not ''vec[float]''. The only subtyping is the bare form: a subtype may **narrow** a ''vec[]'' return to a concrete ''vec[int]'' and **widen** a ''vec[int]'' parameter to ''vec[]''. ===== Construction and validation ===== A literal evaluates every element **left to right**, then validates each against the declared member type; publishing the value is a single final step, so no partially built value is ever observable. vec[int]{1, 2, 3}; tuple[int, string]{200, "OK"}; set[int]{1, 2, 2, 3}; // → {1, 2, 3} **Constructible element types.** A type accepted as a //declaration// is not automatically one a //value// may hold. A value may hold: the leaf scalars ''int'' / ''float'' / ''string'' / ''bool'' / ''array'', a single class/interface name, and a nested constructible collection (all three kinds, including ''set[vec[int]]''). Every other accepted member type — ''?int'', ''int|string'', ''A&B'', ''mixed'', ''object'', ''callable'', ''null'', ''false'' — parses and type-checks but **cannot hold a value**, and constructing such a literal fails //before any element is evaluated//: vec[?int]{1}; // TypeError: Cannot create a value of type vec[?int] vec[mixed]{1}; // TypeError: Cannot create a value of type vec[mixed] ''vec[?int]'' is thus a legal //type// even though ''vec[?int]{…}'' is a runtime error. **Validation is strict and coercion-free, independent of ''strict_types''** — no scalar coercion, and a class member accepts subtypes only. The error names the position, the collection type, and the expected vs. offending type: vec[int]{1, "2"}; // TypeError: Element 1 of vec[int] must be of type int, string given vec[float]{1}; // TypeError: Element 0 of vec[float] must be of type float, int given vec[Animal]{new Dog()}; // ok (Dog is an Animal); vec[Dog]{new Animal()} is a TypeError **Failure atomicity.** All elements are evaluated before validation and publication; if an element expression throws, no value is produced and the assignment target is untouched. Side effects of already-evaluated elements are retained, because PHP cannot unwind them. A collection literal is **not permitted in a constant expression** (''const'', class constant, parameter/property default, ''enum'' case, attribute argument); these fail at compile time with *"Collection literals are not allowed in constant expressions."* Ordinary runtime initializers (e.g. ''static $x = vec[int]{…}'') are unaffected. ===== Common semantics ===== ==== Immutability ==== A collection cannot be modified in place. ''$v[0] = …'', ''$v[] = …'' and ''unset($v[0])'' are not operations on a collection. Immutability is **shallow**: a mutable //object// stored as an element stays mutable, but the collection's length, order and slots are fixed. ==== Identity and comparison ==== ''==='' / ''!=='' is **recursive structural value identity**: two collections are identical iff they have the **same type** (kind + member types) and **strictly identical elements** — ''vec''/''tuple'' positionally, ''set'' **order-insensitively** — recursing into nested collections. Elements compare with ordinary ''===''. vec[int]{1, 2} === vec[int]{1, 2}; // true vec[int]{1, 2} === vec[int]{2, 1}; // false (vec/tuple are positional) set[int]{1, 2} === set[int]{2, 1}; // true (set identity is order-insensitive) vec[int]{1} === vec[float]{1.0}; // false (different type) vec[int]{1, 2} === [1, 2]; // false (never === a non-collection) Loose and ordering comparisons — ''=='', ''!='', ''<'', ''<='', ''>'', ''>='', ''<=>'', sorting, and non-strict ''in_array'' — **throw** `TypeError: Cannot compare collection values`. A collection has a well-defined equality but no natural ordering; use ''===''. (Strict ''!== null'' / ''!== false'' / ''!== true'' behave normally.) ==== Truthiness and conversions ==== * A collection is **always truthy**, empty or not. * ''(int)'' / ''(float)'' and arithmetic **throw**; ''(string)'' / ''echo'' / concatenation **throw**. * ''json_encode()'' returns ''false'' (a collection has no JSON representation in this RFC). * ''gettype()'' and ''get_debug_type()'' return ''"collection"''; the parameterised name (''vec[int]'') is produced by ''var_dump'', ''var_export'' and diagnostics. ==== References and contained objects ==== A collection is a value, not an object, so ''clone'' does not apply. A collection cannot hold a PHP reference as an independently mutable slot: a reference element is read by value. There is **no implicit conversion** between ''array'' and any collection kind in either direction. ===== Collection kinds ===== ==== ''vec'' ==== ''vec[T]'' is an ordered, densely indexed (from 0) sequence with a single element type ''T''. A ''vec'' element may itself be a constructible collection. $v = vec[int]{10, 20, 30}; $nested = vec[vec[int]]{ vec[int]{1, 2}, vec[int]{3} }; ==== ''set'' ==== ''set[T]'' is a collection of distinct ''T''. Distinctness is decided by the same structural ''===''. Duplicates are **silently dropped** (that is set semantics, not an error) and the **first occurrence** is kept; iteration visits values in **first-occurrence order**, but set identity is order-insensitive, so iteration order is not part of the value. set[int]{1, 2, 2, 3, 1}; // {1, 2, 3} set[vec[int]]{ vec[int]{1, 2}, vec[int]{1, 2}, vec[int]{3} }; // 2 distinct members ==== ''tuple'' ==== ''tuple[A, B, …]'' is a fixed-arity record with a declared type per position; element //i// is validated against member //i//. The element count must equal the declared arity **exactly**, and — because arity is statically known — a mismatch is a **compile-time** error: tuple[int, string]{200, "OK"}; // ok tuple[int, string]{200}; // Fatal error: Collection type tuple expects 2 elements, 1 given ===== Reading collections ===== ==== Indexing ==== ''$v[$i]'' reads the element at integer position ''$i'' from a ''vec'' or ''tuple'' (read-only): (vec[int]{10, 20, 30})[1]; // 20 (tuple[int, string]{1, "a"})[1]; // "a" * **''int'' only** — ''$v["1"]'', ''$v[1.5]'', ''$v[true]'', ''$v[null]'' throw ''TypeError''; even ''$v["0"]'' is a strict-int miss, not index 0. * **Out-of-range or negative** throws ''ValueError'' (never a warning + ''null''). * **Read-only** — ''$v[0] = …'', ''$v[] = …'', ''++$v[0]'', ''unset($v[0])'', ''&$v[0]'' all throw and never mutate. * ''isset()'' / ''empty()'' / ''??'' and ''list()'' destructuring are total (never throw) and strict-int. * A ''set'' is **not** positionally indexable. Indexing is native (not ''ArrayAccess''). ==== Iteration and ''iterable'' ==== ''foreach'' iterates **by value**: ''vec''/''tuple'' yield ''int'' keys ''0..n−1'' in order, ''set'' yields keys over its first-occurrence order. A contained array is copy-on-write, an object keeps its identity, a nested collection stays immutable. **Iterating by reference throws** (''Cannot iterate over vec by reference''). foreach (vec[int]{10, 20, 30} as $k => $v) { /* 0=>10 1=>20 2=>30 */ } foreach (set[int]{3, 1, 3, 2} as $v) { /* 3, 1, 2 */ } Collections satisfy the **''iterable''** type: ''is_iterable($c)'' is ''true'', an ''iterable'' parameter/return accepts a collection, and ''iterator_to_array()'' / ''iterator_count()'' work. A collection is **not** ''Traversable'' and not an object — it is iterable //as a value//, like an ''array''. ==== Cardinality ==== Cardinality is read through two read-only **intrinsic properties**, ''->count'' and ''->isEmpty''. They are a closed, case-sensitive set (any other name throws) and read-only (assignment / ''++'' / ''unset'' throw): (vec[int]{1, 2, 3})->count; // 3 (vec[int]{})->isEmpty; // true (set[int]{1, 1, 2})->count; // 2 (after de-duplication) The global ''count()'' function is not extended in this RFC (see Future scope). ===== Immutable update methods ===== New collections are produced from existing ones through **methods**; each returns a //new// collection and never mutates the receiver. Method dispatch does not turn the value into an object (''is_object($c)'' stays ''false''); the methods are chainable, first-class-callable (''$c->append(...)'') and reflectable. $v = vec[int]{1, 2, 3}; $v->append(4); // vec[int]{1,2,3,4} ($v unchanged) $v->prepend(0); // vec[int]{0,1,2,3} $v->withAt(1, 99); // vec[int]{1,99,3} $v->withoutAt(0); // vec[int]{2,3} $s = set[int]{1, 2, 3}; $s->with(4)->without(2); $s->union(set[int]{3, 4, 5}); // {1,2,3,4,5} $s->intersect(set[int]{2, 3, 9}); // {2,3} $s->diff(set[int]{2}); // {1,3} $t = tuple[int, string]{1, "a"}; $t->withAt(1, "b"); // tuple[int,string]{1,"b"} (arity preserved) ^ Receiver ^ Signature ^ Meaning ^ | ''vec[T]'' | ''append(T $value): vec[T]'' | ''$value'' added at the end | | ''vec[T]'' | ''prepend(T $value): vec[T]'' | ''$value'' added at the front | | ''vec[T]'' | ''withAt(int $i, T $value): vec[T]'' | index ''$i'' replaced | | ''vec[T]'' | ''withoutAt(int $i): vec[T]'' | index ''$i'' removed (the rest close up) | | ''tuple[…]'' | ''withAt(int $i, T $value): tuple[…]'' | position ''$i'' replaced (''T'' = the type at ''$i''; arity fixed) | | ''set[T]'' | ''with(T $value): set[T]'' | a ''set'' that also contains ''$value'' | | ''set[T]'' | ''without(T $value): set[T]'' | a ''set'' without ''$value'' | | ''set[T]'' | ''union(set[T] $other): set[T]'' | the union of the two sets | | ''set[T]'' | ''intersect(set[T] $other): set[T]'' | the intersection of the two sets | | ''set[T]'' | ''diff(set[T] $other): set[T]'' | the elements in this ''set'' but not ''$other'' | Here ''T'' is **notation, not PHP syntax** — PHP has no generics. ''T'' denotes the receiver's **element type** (for ''tuple::withAt'', the type declared at position ''$i''), and ''vec[T]'' / ''set[T]'' / ''tuple[…]'' denotes the receiver's **exact** type. * At the **Reflection** level everything is erased: each ''T'' parameter is declared **''mixed''** (the ''union'' / ''intersect'' / ''diff'' operands are declared **''set[]''**), and every return type reflects as the erased bare kind (e.g. ''vec[]'', ''set[]''). The element/member type is **enforced at runtime** — a mismatching value throws **''TypeError''**, and an out-of-range index (''withAt'' / ''withoutAt'') throws **''ValueError''**. * The **returned value** still carries the receiver's exact type — a ''vec[int]->append(2)'' is a ''vec[int]'' — even though its reflected return type is erased. A tuple's arity is preserved, so it has only ''withAt''. * Set membership, de-duplication and the set operations all compare with ''===''. ''tuple'' has no append/remove because changing arity would produce a //different// type. Functional/bulk methods (''map'', ''filter'', …) are **not** part of this RFC (see Future scope). ===== Serialization and display ===== ''serialize()'' and ''unserialize()'' round-trip every kind, using one new, versionless serialize token. ''unserialize()'' rebuilds the type and **revalidates every element** through the same path a literal uses, so it grants no new trust: malformed input, an unknown or unconstructible kind, a bad element, or a bad count/arity all fail cleanly (''unserialize()'' returns ''false''). Existing serialized data is unaffected. ''var_export()'' emits re-parseable collection-literal syntax; ''var_dump()'', ''print_r()'' and ''debug_zval_dump()'' render the parameterised type and elements. var_export(vec[int]{1, 2, 3}); // vec[int]{ 1, 2, 3, } (re-parseable) ===== Reflection ===== A collection type is reflected through a dedicated ''ReflectionCollectionType'' (extending ''ReflectionType''): $t = (new ReflectionFunction(fn(vec[vec[string]] $x) => null))->getParameters()[0]->getType(); $t instanceof ReflectionCollectionType; // true $t->getCollectionName(); // "vec" (string) $t; // "vec[vec[string]]" $t->allowsNull(); // false $t->getTypes(); // [ ReflectionCollectionType for vec[string] ] ''getCollectionName()'' returns the kind; ''getTypes()'' returns the member types (and the empty array ''[]'' for a bare type). ''getTypes()'' generalises to multi-member kinds. ===== Error behavior ===== ^ Condition ^ Error ^ When ^ | Element does not satisfy the member type | ''TypeError'' (''Element N of T must be of type …, … given'') | construction (runtime) | | Member type has no value representation (''?int'', ''mixed'', union, …) | ''TypeError'' (''Cannot create a value of type …'') | before elements (runtime) | | ''tuple'' element count ≠ arity | Fatal error (''… expects N elements, M given'') | compile time | | Wrong type-argument count (''vec[int, string]'') | Fatal error (''… expects 1 parameter, 2 given'') | compile time | | Collection type in a union/intersection | Fatal error | compile time | | Collection literal in a constant expression | Fatal error | compile time | | Index of wrong type / out of range | ''TypeError'' / ''ValueError'' | runtime | | Loose or ordering comparison of collections | ''TypeError'' (''Cannot compare collection values'') | runtime | | ''foreach'' by reference | ''Error'' (''Cannot iterate over … by reference'') | runtime | | Numeric/string conversion | ''TypeError'' | runtime | | Update method: bad element / out-of-range index | ''TypeError'' / ''ValueError'' | runtime | ===== Backward compatibility ===== * **No identifiers are reserved.** ''vec'', ''set'', ''tuple'' (and the future ''map'', ''shape'') remain usable as function, class, constant, method and property names and in member access (''$o->vec'', ''Foo::vec''). Only the adjacent two-token sequence ''name['' in type/expression position is reserved. * **The single BC break** is a bare constant dereference where the constant is named after a kind and is immediately followed by ''['' — ''vec[0]'' with ''vec'' a ''const''. Both workarounds continue to work: ''vec [0]'' (with a space) and ''(vec)[0]''. A corpus scan found no such occurrences; a broader ecosystem sweep is a stated prerequisite before the vote. * **Serialization** adds one token and is additive; existing serialized data is unaffected. * **Extensions and debuggers** that switch exhaustively on zval types need one new arm for the collection tag. * **No implicit ''array''↔collection conversion** exists in either direction. ===== Rejected alternatives ===== * **A userland ''Collection'' library.** PHP has no generics, so an //enforced// element type and a distinct runtime type cannot be expressed in userland. * **''readonly'' classes.** They give shallow immutability for a nominal object, but not a structural type, an enforced element type, or set/tuple semantics; every shape would be a hand-written class. Complementary, not a substitute. * **Overloading ''array''.** Making ''array'' element-typed at runtime is infeasible for the language's most-used type; a distinct value type keeps ''array'' untouched. * **Operators for set algebra** (''+'', ''|'', ''&'', ''-'') and comparison (''=='', ''<=>''). ''+'' on arrays already means key-union; ''|''/''&''/''-'' are bitwise and would require an operator-overloading model PHP does not have and merely duplicate the named ''union''/''intersect''/''diff''; ''==''/''<=>'' on collections throw by design. The named methods and ''==='' are the surface. * **A general ''$value with { … }'' update expression.** Set aside in favour of methods for this RFC; it is a broader, cross-type feature (see Future scope). ===== Future scope ===== The following are **not** proposed here and are independently decidable later. Detailed design analysis for these items is kept in separate design notes, not in this RFC. * **''map'' and ''shape''** — separate, independently votable future RFCs. ''map'' needs a keyed representation and a ''key => value'' grammar; ''shape'' is blocked on a record-vs-ordered decision. * **Functional / bulk methods** — ''map'', ''filter'', ''slice'', ''reverse'', ''flatMap'', ''groupBy'', ''sorted'', ''reduce'', ''any'', ''all'', ''find'', ''contains''. The type-changing ones (notably ''map'') need a mechanism to determine and preserve the result element type that PHP does not yet have; the analysis is in the architecture document. * **Contextual literal inference** (''vec{1, 2, 3}'', inferring the element type from context). Prototyped and set aside: the argument position needs a VM change to be sound under opcache. (Architecture §10.) * **A general ''$value with { … }'' update expression** — a future cross-type, non-destructive-update RFC. * **Global ''count()'' / ''is_countable()'' support** — the ''->count'' property covers cardinality for now. * **Union / intersection / nullable member types** in constructible //values//, and **collection literals in constant expressions** — both forward-compatible extensions. * **Integration with a future generics feature**, should one arrive — these types are designed to coexist with, not pre-empt, such a design. ===== Proposed PHP version ===== **PHP 9.0** (8.6 is already at feature freeze). ===== Implementation ===== Implemented on the [[https://github.com/brzuchal/php-src/tree/first-class-collections|''first-class-collections'']] branch of the author's php-src fork; a pull request to php-src will follow. The engine-level design (runtime representation, descriptors and canonicalization, the construction pipeline, set de-duplication, garbage collection, the serialization wire format, and opcache/JIT integration), the current build and platform status, and the test suite are documented separately and accompany the implementation. The implementation includes coverage for parsing, type declarations, construction, value semantics, iteration, indexing, the update methods, serialization, Reflection, and opcache/JIT. ===== Vote ===== The vote requires a two-thirds majority. > Add native immutable collection value types — ''vec'', ''set'' and ''tuple'' — as > described in this RFC? Secondary votes may be offered for independently removable parts (for example initial Reflection support, or the target version) if reviewers request them. ---- ===== References ===== * Implementation — [[https://github.com/brzuchal/php-src/tree/first-class-collections|php-src branch ''first-class-collections'']] (author's fork; a php-src pull request will follow) * ''map'' and ''shape'' — separate future RFCs, not yet proposed * Engine design, implementation status and the test inventory are maintained separately and accompany the implementation