PHP RFC: Structs
- Version: 0.9
- Date: 2020-03-25
- Author: Michał Brzuchalski brzuchal@php.net
- Status: Draft
- First Published at: http://wiki.php.net/rfc/structs
Structures as a complex type solution for programmers in functional programming appears in few programming languages but differs in a solution taken there. Some languages like C# have both classes and scructs living aside in harmony.
In PHP we do have a way to express complex types using classes which additionally can implement a behaviour but in some cases, a complex type needs are nothing more than the strict list of fields and type restrictions and easy way to initialize the value reducing the boilerplate on the user side.
Aim of this RFC is to fill the gap which currently cannot be implemented using classes with respect to reducing verbosity to a minimum.
Introduction
Many programming languages with strong support for functional programming has a way to define complex types by the programmer. These complex types are known as structures.
Programming languages with strong OOP paradigm uses classes as enhancements of structures cause they also consist of attached logic and different rules when instantiation, modification and assigning of those types happen.
In languages like C, C++, C# and D structures are value types which means when a struct is passed as an argument to a function any modifications to the struct in that function will not be reflected in the original variable (unless pass-by-reference is used).
Proposal
A struct is a user-defined type that contains a collection of named fields. A structure has different fields of the same or different type. It is used to group related data together to form a single unit.
A struct is a user type consisting of a name and fields specification consisting of field names and corresponding types. A struct cannot be null, and a struct variable cannot be assigned null unless the variable is declared as a nullable value type.
Differences between classes and structs
Structures differ from classes therefore they're not designed for inheritance. Therefore it is not possible to extend struct type as in classes.
Unlike classes, structs can be instantiated without using a new operator and are passed over using by-value semantics.
Usage
A struct is used mainly when you need to define a complex type with no need for any additional logic going along with the value. Therefore a struct is defined to not have an identity and that is why the implementation is free to make copies of the struct as convenient.
Value types
Structs are copied on assignment. When a struct is assigned to a new variable, unlike classes, which are reference types, all the data is copied, and any modification to the new copy does not change the data for the original copy.
Declaration
A struct type is nothing but a schema containing the blueprint of a data a structure will hold. Structs share most of the same syntax as classes. The name of the struct must be a valid PHP identifier name.
To make things simple, it is possible to use type alias so that can refer to struct type easily. The syntax to create a struct is as follows:
struct StructName { fieldType1 $field1; fieldType2 $field2, $field3; }
In the above syntax, `StructName` is a struct type while `$field1`, `$field2` and `$field3` are fields of `fieldType1` and `fieldType2` respectively.
struct Employee { string $firstName; string $lastName; int $salary; bool $fullTime; }
It is also possible to combine different fields of the same type in the same line and with default values as it is possible when declaring class properties, which is presented in the next example:
struct Salary { int $salary = 1000, $insurance = 50, $allowance = 50; } struct Employee { string $firstName, $lastName; Salary $salary = Salary { 1200, 0, 0 }; bool $fullTime = true; }
Initialization
Concluding initialization of struct fields values can be done in a few ways:
- using default initializer with all values for fields in the right order enclosed by a curly brace without field names or with field names (then the order of them has no meaning);
- using custom initializer with values for fields passed in the order specified by initializer function as a values list enclosed in parentheses.
Working with struct without default values initialization could be hard when the amount of fields inside the struct is significant and some of them could possibly have default values cause they're not used so often.
Given that a field default value initialization can benefit, as follows:
$ross = Employee { firstName = "Ross", lastName = "Bing", };
Above example shows the initialization of struct named `Employee` with some default values used to initialize `$salary` and `$fullTime` fields as they're declared in the previous section.
Note! Initialization of default value for `$salary` field of type `Salary` was used when declaring `Employee` struct. This is possible only when struct fields consist only with scalar types (expressions are not allowed cause they can infer global state).
struct Point { int $x = 0, $y = 0; } $point = Point; $point->x = 10; $point->y = 20;
Initializing default values for struct fields allows us to initialize struct with default initialization and initializing fields in separated statements what is presented in the above example.
In some cases like a named struct `Point` where it becomes quite intuitive that there are `x` and `y` fields initialization can be reduced to passing only values in a comma-separated list as shown below:
$point = Point { 10, 20};
Some programming languages have custom struct initializers. In *PHP* this would conflict with functions syntax cause usually custom initializers are functions and therefore they use parentheses with a list of function arguments.
A solution for that may be the use of function with the same name as struct alias to declare such initializer outside of struct declaration. Given that it is possible to define a function, as follows:
function Employee( string $firstName, string $lastName, Salary $salary = Salary { salary = 1200, insurance = 0, allowance = 0 } ): Employee { return Employee { firstName = $firstName, lastName = $lastName, salary = $salary, }; } $ross = Employee("Ross", "Bing");
To satisfy all kind of needs like custom initialization using different input types we can go with a factory class, as follows:
use Symfony\Component\HttpFoundation\Request; class EmployeeFactory { public function fromRequest(Request $request): Employee { return Employee { firstName = $request->request->get("first_name"), lastName = $request->request->get("last_name"), salary = Salary { salary = $request->request->getInt("salary"), insurance = $request->request->getInt("insurance"), allowance = $request->request->getInt("allowance"), }, fullTime = $request->request->getBoolean("full_time"), }; } public function fromArray(array $data): Employee { return Employee { firstName = $data["first_name"], lastName = $data["last_name"], salary = Salary { salary = (int) $data["salary"], insurance = (int) $data["insurance"], allowance = (int) $data["allowance"], }, fullTime = (bool) $data["full_time"], }; } } $factory = new EmployeeFactory(); $request = Request::createFromGlobals(); $employee = $factory->fromRequest($request); $john = $factory->fromArray([ "first_name" => "John", "last_name" => "Doe", "salary" => 1000, "insurance" => 100, "allowance" => 100, "full_time" => true, ]);
Note! In the above example factory class has a different name than the struct itself - this differs from the previous example with a function named `Employee` because a class is a user-defined type and functions are not types. Naming class with the same name would collide in symbol resolution.
This leads to one small implication. When types are defined inside a namespace and we want to use short names we need to add direct `use` clauses.
namespace Accounting { struct Salary { int $salary = 1000, $insurance = 50, $allowance = 50; } struct Employee { string $firstName, $lastName; Salary $salary = Salary { salary = 1200, insurance = 0, allowance = 0 }; bool $fullTime = true; } function Employee( string $firstName, string $lastName, Salary $salary = Salary { salary = 1200, insurance = 0, allowance = 0 } ): Employee { return Employee { firstName = $firstName, lastName = $lastName, salary = $salary, }; } } namespace App { use struct Accounting\Salary; use function Accounting\Employee; $ross = Employee("Ross", "Bing", Salary { 1200, 100, 100 }); }
Above example show the use of function name `Employee` from another namespace and the best what we can see in all above examples *PHP* gives us plenty of ways allowing to implement custom initialization mechanisms without the need to add any logic onto structs.
Anonymous structs
In some cases, we don't need a struct to be named and all we need is it to present a kind of tuple with fields named and specific types guarded. This can be solved by introducing anonymous structs. Internally the struct definition is created in a similar way to anonymous classes, they do have a generated name but that is simply not exposed to the userland.
$monica = struct { string firstName = "Monica", string lastName = "Gellard", int salary = 1200, bool fullTime = true, };
Fields composition
An interesting feature of the struct is the inclusion of the fields allowing to compose complex structs with the use of already existing structs definition:
struct Salary { int $salary = 1000, $insurance = 50, $allowance = 50; } struct Person { string $firstName, $lastName; } struct Employee { Person; Salary; } $ross = Employee { firstName: "Ross" lastName: "Bing" salary = 1200, insurance = 0, allowance = 0, } println( "%s's basic salary is %d, insurance is %d and allowance is %d", $ross->firstName, $ross->salary, $ross->insurance, $ross->allowance );
In the above example of the nested struct, we removed `salary` field name and just used `Salary` struct type to include fields from it into `Employee` the same goes for including of fields from `Person`.
Backward Incompatible Changes
No BC breaks.
Proposed PHP Version(s)
Next PHP 8.x.
RFC Impact
To SAPIs
None
To Existing Extensions
No
To Opcache
Should be verified
New Constants
None
Future Scope
Casting
Casting structs can be a future enhancement of structs proposal. Due to all struct fields being visible and their type exposed it would be possible to add “down-casting” to any kind of struct if both field names and types matches, like in example below:
struct Salary { int $salary = 1000, $insurance = 50, $allowance = 50; } struct Person { string $firstName, $lastName; } struct Employee { Person; Salary; } $ross = Employee { firstName: "Ross" lastName: "Bing" salary = 1200, insurance = 0, allowance = 0, } $person = (Person) $ross; println( "Person %s %s", $person->firstName, $person->lastName );
Proposed Voting Choices
The primary vote requires 2/3.
Patches and Tests
TBD.
Implementation
TBD.