[Proposal] Struct Data Types

php.internals

Kenneth Ellis McCall

7 years ago
Hey all, I'm looking to get feedback on a RFC I want to propose. PHP RFC: Addition of the 'struct' data type. Introduction: PHP has many data types, but does not offer something that is the equivalent to the C struct data type. The purpose of this RFC is to propose a data type of 'struct', which would be a strictly typed, immutable data structure that resembles a mix of a class and an array. Pros: Provides a data type which would immutable and self-validating by mandatory type hinting. Cons: ? Possible future considerations: Advanced Type Validators. Use cases: Configurations, DTOs, anything that needs a strict schema. Proposed usage / syntax: struct MyStuct { boolean|bool 'prop01'; integer|int 'prop02'; double 'prop03'; string 'prop04'; array 'prop05'; object 'prop06'; resource 'prop07'; callable 'prop08'; iterable 'prop09'; Acme\MyClass 'prop10'; Acme\ConfStruct 'prop11'; array Acme\AnotherStruct 'prop12'; ?boolean|bool 'prop13'; ?integer|int 'prop14'; ?double 'prop15'; ?string 'prop16'; ?array 'prop17'; ?object 'prop18'; ?resource 'prop19'; ?callable 'prop20'; ?iterable 'prop21'; ?Acme\MyClass 'prop22'; ?Acme\ConfStruct 'prop23'; ?array Acme\AnotherStruct 'prop24'; } OR? struct MyStuct { 'prop01': boolean|bool; 'prop02': integer|int; 'prop03': float; 'prop04': string; 'prop05': array; 'prop06': object; 'prop07': resource; 'prop08': callable; 'prop09': iterable; 'prop10': Acme\MyClass; 'prop11': Acme\ConfStruct; 'prop12': array Acme\AnotherStruct; 'prop13': ?boolean|bool; 'prop14': ?integer|int; 'prop15': ?float; 'prop16': ?string; 'prop17': ?array; 'prop18': ?object; 'prop19': ?resource; 'prop20': ?callable; 'prop21': ?iterable; 'prop22': ?Acme\MyClass; 'prop23': ?Acme\ConfStruct; 'prop24': ?array Acme\AnotherStruct; } struct Acme\ConfStruct { string 'host'; ?int 'port'; } struct Acme\AnotherStruct { string 'firstName'; string 'lastName'; ?integer 'age'; } Acme\ConfStruct $a = { 'host' => '127.0.0.1', 'port' => 8088 } Acme\MyStruct $myStruct = { 'prop01' => true, 'prop02' => 1, 'prop03' => 1.01, 'prop04' => 'Hello', 'prop05' => ['a', 'b', 'c'], 'prop06' => new stdClass(), 'prop07' => fopen('xyz.txt', 'wb'), 'prop08' => function() { return 1 + 1; }, 'prop09' => [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ], 'prop10' => new Acme\MyClass, 'prop11' => $a, 'prop12' => [ Acme\AnotherStruct [ 'firstName' => 'Bob', 'lastName' => 'Walker' ], Acme\AnotherStruct [ 'firstName' => 'Little', 'lastName' => 'Johnny', 'age' => 5 ], ], 'prop13' => null, 'prop14' => null, 'prop15' => null, 'prop16' => null, 'prop17' => null, 'prop18' => null, 'prop19' => null, 'prop20' => null, 'prop21' => null, 'prop22' => null, 'prop23' => null, 'prop24' => null }; $x = new Some\Other\Class($myStruct); echo $myStruct['prop02']; OR? echo $myStruct->prop02; Warning / Errors / Exceptions: Acme\MyStruct $a = { 'prop01' => 5, ... } // Should: throw new TypeError("'prop01' should be set as a boolean type, integer type was attempted.") echo $a['xyz']; // or echo $a->xyz (which ever is the preferred syntax after comments). // Should: trigger_error("<b>Notice</b>: Undefined property: Acme\MyStruct::xyz"); https://github.com/ellisgl/PHP-RFC-Struct-Data-Type

Kenneth Ellis McCall

7 years ago
Kenneth Ellis McCall wrote:
> Hey all, > > I'm looking to get feedback on a RFC I want to propose. > > PHP RFC: Addition of the 'struct' data type. >
Hey all, Hopefully this addresses the questions you had in someway, even if it's not a direct answer and might propose other things. Also sorry for the brevity. Immutability: My initial thought was to have the property list be immutable, such as you can't add or remove properties, but could change the value. Maybe it would be possible to do something like this: const Acme\MyStruct $myStruct = { ... }; To make just the instance of the struct be fully immutable. Other options could be another type, say constStruct (say that ten times fast), or maybe add a const hint type (or readonly) to the properties, like: struct Acme\MyStruct { const int id; } Access: I think I'm leaning towards the arrow accessors. Making it more of class type: Initial thinking, and why I thought we would need this, is that I would like a way to not use class objects for data bags (like that you see with entities), since you can abuse classes (and arrays) like this: class xyz { }; $a = new xyz(); $a->def = 123; // Returns 123. echo $a->def; While people shouldn't do it, but because they can, they will. Types hints: I think having them on the left side would be the best, since it kind of matches what we already do. As for the loosely type properties, I'm kind of on the border on that. I really want to have strongly / statically typed hints, so it could enforce some better habits. On the other side, for historical purposes... Would definitely want to take a consensus on this. Validation: I have another item I want to bring up, but don't think it would go over well: https://github.com/ellisgl/PHP-RFC-Advanced-Type-Hint-Validors Array / Class features: When I wrote, 'resembles a mix of a class and an array' I was thinking loosely around the styling, accessing properties and errors. Copy on Write or pass by reference/value: I think I'm more with Levi with the "pass by-value with copy-on-write semantics", of course, since this is still draft mode, it could go another way after further investigation and testing during implementation.

Larry Garfield

7 years ago
On Fri, Mar 15, 2019, at 10:56 PM, Kenneth Ellis McCall wrote:
> Kenneth Ellis McCall wrote: > > Hey all, > > > > I'm looking to get feedback on a RFC I want to propose. > > > > PHP RFC: Addition of the 'struct' data type. > > > > Hey all, > > Hopefully this addresses the questions you had in someway, even if it's > not a direct answer and might propose other things. Also sorry for the > brevity. > > Immutability: > My initial thought was to have the property list be immutable, such as > you can't add or remove properties, but could change the value. > Maybe it would be possible to do something like this: > > const Acme\MyStruct $myStruct = { > ... > }; > > To make just the instance of the struct be fully immutable. > > Other options could be another type, say constStruct (say that ten times > fast), or maybe add a const hint type (or readonly) to the properties, like: > > struct Acme\MyStruct { > const int id; > } > > > > Access: > I think I'm leaning towards the arrow accessors. > > > Making it more of class type: > Initial thinking, and why I thought we would need this, is that I would > like a way to not use class objects for data bags (like that you see > with entities), since you can abuse classes (and arrays) like this: > > class xyz { > > }; > > $a = new xyz(); > $a->def = 123; > > // Returns 123. > echo $a->def; > > While people shouldn't do it, but because they can, they will. > > Types hints: > I think having them on the left side would be the best, since it kind of > matches what we already do. > As for the loosely type properties, I'm kind of on the border on that. I > really want to have strongly / statically typed hints, so it could > enforce some better habits. On the other side, for historical > purposes... Would definitely want to take a consensus on this. > > > Validation: > I have another item I want to bring up, but don't think it would go over > well: https://github.com/ellisgl/PHP-RFC-Advanced-Type-Hint-Validors > > > Array / Class features: > When I wrote, 'resembles a mix of a class and an array' I was thinking > loosely around the styling, accessing properties and errors. > > > Copy on Write or pass by reference/value: > I think I'm more with Levi with the "pass by-value with copy-on-write > semantics", of course, since this is still draft mode, it could go > another way after further investigation and testing during implementation.
Hi Kenneth. I'm a loud proponent of more formally defined data structures in general, so I'm very much on board in concept. However, *almost* all of what you describe either can be done with classes today, has been proposed to add to classes already even if it hasn't passed yet, or could be added to classes just as easily. I've actually started using public-property classes as essentially this sort of struct recently, and for non-API use I quite like it. 1) As of PHP 7.4, class properties can be typed. 2) Union/compound types have been discussed many times; if they ever get adopted they would no doubt be adopted universally and not specific to just one data structure type. (Viz, they'd work on classes, parameter types, and return types, too.) 3) There was a proposal on list less than a week ago to add a "locked" marker for classes to prevent the addition of properties at runtime. The reception was positive although it's obviously not a guarantee to pass. (I'm in favor.) 4) The "set properties in place of a constructor" approach is totally compatible with classes as well, at least in concept. It could also help with the common "my constructor does nothing but set properties to same-named constructor params" pattern. I'm sure someone would propose adding it to classes as well very shortly after it was added to a new "struct" type. 5) IMO, the previously proposed (~2-3 years ago?) property accessor RFC would be a superior way to handle property-level access control. In short, it would allow for Javascript-esque get/set methods that behave like properties, but you can lock down their type and their public/private/protected; thus a private set() method would effectively render a property read-only to the outside world. IIRC it failed mainly due to performance concerns; (I don't know the engine well enough to suggest a way around them at this point.) But that would effectively allow read-only properties on classes. The one exception is the passing and modification semantics, which are two different things. On the one hand, there's the passing semantics. As others have noted, objects currently pass by handle (which means they feel like they're by reference even though they're technically not), while arrays and everything else pass by value. A pass by value "object" has its advantages, no question, but there are ways around that. For example, PSR-7 request/response objects kicked off the idea of "evolvable" objects; their "mutator" method (with*()) only ever clone-and-return-modified (similar to DateTimeImmutable), so even though they technically pass by handle you don't have to worry about spooky-action-at-a-distance. However, that dovetails into the question of property mutability (modification semantics). Assuming object-like syntax for the moment: struct MyStruct { string $foo; int $count = 5; } $m = new MyStruct{foo: 'bar'}; $m->foo = 'baz'; Is that legal? It's definitely not "immutable" behavior by any standard definition of that term. However, if you don't allow that then you have an object that is truly immutable, which means of fairly little use. You'd need to invent some new syntax for "create a new struct instance that is just like this other one except for..." Off the top of my head I can think of two possibilities: $n = $m{ foo: 'baz'}; $n = $m->foo = 'baz'; In both cases, $n is now foo: 'baz', count: 5. Both feel kind of icky to me, even if the second weren't highly confusing with object syntax. Of course, if that were resolved nicely for structs I give it about 12 seconds after structs are approved before someone asks for the same capabilities for classes. Of course, there's a second problem. I give it about another 14 seconds after structs are approved before someone asks for methods on structs, because even with typed properties you can still very easily run into data inconsistency problems; a string that doesn't match a required phone number format, or a property that should update when some other property does, etc. So we'd need to think about adding methods to structs (which is something that C++, Go, Rust, etc. all support... because structs and objects are the same thing in those languages, more or less). At which point... we basically just have objects with PHP4-style passing semantics. :-) So if the net result is that we eventually end up with "structs are objects that pass funny", it would be far less effort and confusion, IMO, to simply allow for "objects that pass funny" as a first class thing; everything else described here either is already the case for objects or if added to structs would get added to objects sooner or later. I don't know that simply adding a byval keyword to a class definition would be the way to go about it; maybe using the `struct` keyword in place of `class` but otherwise being identical? As others have noted there's ample potential for confusion there when using an object/struct that may not pass as intended. In general, though, I think beefing up objects to be "more usable as structs" (locked against dynamic properties, getter/setter methods, possibly some improved tooling for with*()-style behavior, etc.) is the more sustainable long-term solution than adding an entirely new language construct. --Larry Garfield

Rowan Collins

7 years ago
On 16 March 2019 05:18:55 GMT+00:00, Larry Garfield <larry@garfieldtech.com> wrote:
>In general, though, I think beefing up objects to be "more usable as >structs" (locked against dynamic properties, getter/setter methods, >possibly some improved tooling for with*()-style behavior, etc.) is the >more sustainable long-term solution than adding an entirely new >language construct.
Hi Larry, That's a great summary, and I agree that structs make most sense viewed as an object with special behaviours, rather than a completely separate type. Native support for "evolvable" objects (withFoo methods) would be interesting, although I'm not sure what it would look like - ideally, it would reduce both the boilerplate of declaring them, and the overhead of creating intermediate objects when evolving more than one property. It's still more verbose to write than direct assignment to a property, but no more so than calling a setter method explicitly. One thing that hasn't been brought up yet is identity: if structs are just a special kind of object, you would be able to compare them by identity; if they are more like arrays, you would only be able to compare them by value. I think not having identity would make sense, both because of the expected use cases, and because it plays better with copy-on-write optimisations: $x = new SomeStruct { foo=0 }; $x = $y; // copy-by-value, but optimised internally to copy-on-write var_dump($x === $y); // true? at this point, there is no new object identity $y->foo = 1; // copy-on-write triggers internally var_dump($x === $y); // false? they are now separated $y->foo = 0; var_dump($x === $y); // false? structs are now identical again, but separated in memory This is much more intuitive if structs have no identity, and any two instances with the same values are considered identical. Regards,
-- Rowan Collins [IMSoP]

Larry Garfield

7 years ago
On Sat, Mar 16, 2019, at 4:43 AM, Rowan Collins wrote:
> On 16 March 2019 05:18:55 GMT+00:00, Larry Garfield > <larry@garfieldtech.com> wrote: > >In general, though, I think beefing up objects to be "more usable as > >structs" (locked against dynamic properties, getter/setter methods, > >possibly some improved tooling for with*()-style behavior, etc.) is the > >more sustainable long-term solution than adding an entirely new > >language construct. > > > Hi Larry, > > That's a great summary, and I agree that structs make most sense viewed > as an object with special behaviours, rather than a completely separate > type. > > Native support for "evolvable" objects (withFoo methods) would be > interesting, although I'm not sure what it would look like - ideally, > it would reduce both the boilerplate of declaring them, and the > overhead of creating intermediate objects when evolving more than one > property. It's still more verbose to write than direct assignment to a > property, but no more so than calling a setter method explicitly.
Ruminating on this a while, I'm not sure there's any good way to do it until/unless we have property accessor methods. With a value object, you very often do want/need to maintain some internal validity. There's ample cases where only allowing mutation/evolution (sounds like the Zerg from Starcraft) through a method is a requirement, so any approach to first class value objects will need to allow for that. Without adding special syntax, with*() methods already do that well enough that there's little need for further syntax help. At best we could automate the $that = clone($this) first line of such methods but that's not really worth the effort. The only reasonable way I see to have both with*()-style method support and value-object behavior would be something like this: $obj2 = $obj1{foo: 'bar'}; Where $obj2 is now a totally new object (give or take internal CoW behavior that is just an optimization) where the foo property is now set to 'bar'; however, if there is a setter method defined for foo then that gets called; which means the setter must be public (which raises other issues), and can do whatever additional logic or validation is needed. So the following two classes would have essentially the same semantic effect: class Stuff { protected $foo; public function withFoo($val) : Stuff { assert($val > 10); $new = clone($this); $new->foo = $val; return $new; } } $s = new Stuff(); $s2 = $s->withFoo('bar'); struct Stuff { protected $foo public set($val) { assert($val > 10); $this->foo = $val; } } $s = new Stuff{}; $s2 = $s{foo: 'bar'}; Whether that's a good approach or not I don't now, but it's the only one that I could see working. And just looking at the sample code above I see a lot of potential for very bad things.
> One thing that hasn't been brought up yet is identity: if structs are > just a special kind of object, you would be able to compare them by > identity; if they are more like arrays, you would only be able to > compare them by value. I think not having identity would make sense, > both because of the expected use cases, and because it plays better > with copy-on-write optimisations: > > $x = new SomeStruct { foo=0 }; > $x = $y; // copy-by-value, but optimised internally to copy-on-write > var_dump($x === $y); // true? at this point, there is no new object > identity > $y->foo = 1; // copy-on-write triggers internally > var_dump($x === $y); // false? they are now separated > $y->foo = 0; > var_dump($x === $y); // false? structs are now identical again, but > separated in memory > > This is much more intuitive if structs have no identity, and any two > instances with the same values are considered identical.
I agree you'd expect value structs/objects to equate like primitives, not like pointers. Perhaps this is an argument for an __equals() method? Or the previously-rejected comparison magic method RFC? --Larry Garfield