Re: [RFC][Vote] Typed Properties

php.internals

Unnamed Person

10 years ago
I'm not seeing a problem here: class A { public int $x; public ?int $y = null; public int $z = 42; public ?int $u; public ?datetime $v; public datetime $w; } $a = new A; var_dump($a->x); // 0 + notice var_dump($a->y); // null var_dump($a->z); // 42 var_dump(isset($a->z)); // true unset($a->z); var_dump(isset($a->z)); // false var_dump($a->z); // 0 + notice var_dump($a->u); // null + notice var_dump($a->v); // null + notice var_dump($a->w); // Fatal error, uninitialized... var_dump(isset($a->x)); // false var_dump(isset($a->y)); // false var_dump(isset($a->u)); // false var_dump(isset($a->v)); // false var_dump(isset($a->w)); // false Regards Thomas
>> public ?int $y = null; >> public int $z = 42; >> public ?int $u; >> public ?datetime $v; >> public datetime $w; >> } >> >> $a = new A; >> var_dump($a->x); // 0 + notice >> var_dump($a->y); // null >> var_dump($a->z); // 42 >> unset($a->z); >> var_dump($a->z); // 0 + notice >> var_dump($a->u); // null + notice >> var_dump($a->v); // null + notice >> var_dump($a->w); // Fatal error, uninitialized...
Fleshgrinder wrote on 25.05.2016 23:38:

Lester Caine

10 years ago
On 25/05/16 22:52, Thomas Bley wrote:
> var_dump($a->z); // 42 > var_dump(isset($a->z)); // true > unset($a->z);
$a->z does not exist at this point
> var_dump(isset($a->z)); // false
We know that isset is broken so if $a->z is null we also get false ... perhaps the 'fault' that isset does not actually work properly is biting? But since $a->z does not exist false avoids an error.
> var_dump($a->z); // 0 + notice
The notice should be 'variable does not exist' as at this point $a->z has no context at all!
-- Lester Caine - G8HFL ----------------------------- Contact - http://lsces.co.uk/wiki/?page=contact L.S.Caine Electronic Services - http://lsces.co.uk EnquirySolve - http://enquirysolve.com/ Model Engineers Digital Workshop - http://medw.co.uk Rainbow Digital Media - http://rainbowdigitalmedia.co.uk

Unnamed Person

10 years ago
>> var_dump($a->z); // 0 + notice > The notice should be 'variable does not exist' as at this point $a->z > has no context at all!
If we keep the private flag of a property after unset(), we should also keep the type of a property after unset(), e.g. class test { private $z = 42; public function __construct() { unset($this->z); } } $a = new test(); $a->z = 21; // fatal: Cannot access private property... So after unset, $a->z should still have context. Regards Thomas Lester Caine wrote on 26.05.2016 00:13:

Unnamed Person

10 years ago
Of course isset() has a special behaviour. So having some kind of real_isset() or initialized() would be nice. Regards Thomas Thomas Bley wrote on 26.05.2016 00:38:

Tom Worster

10 years ago
On 5/25/16 5:52 PM, Thomas Bley wrote:
> I'm not seeing a problem here: > > class A { > public int $x; > public ?int $y = null; > public int $z = 42; > public ?int $u; > public ?datetime $v; > public datetime $w; > } > > $a = new A; > var_dump($a->x); // 0 + notice > var_dump($a->y); // null > var_dump($a->z); // 42 > var_dump(isset($a->z)); // true > unset($a->z); > var_dump(isset($a->z)); // false > var_dump($a->z); // 0 + notice > var_dump($a->u); // null + notice > var_dump($a->v); // null + notice > var_dump($a->w); // Fatal error, uninitialized... > > var_dump(isset($a->x)); // false > var_dump(isset($a->y)); // false > var_dump(isset($a->u)); // false > var_dump(isset($a->v)); // false > var_dump(isset($a->w)); // false
Is the file containing these examples in liberal mode? What changes if declare(strict_types=1) precedes $a = new A;? Tom

Unnamed Person

10 years ago
I think strict_types=1 should give a fatal error for accessing non-initialized typed properties, instead of notice. Example: declare(strict_types=1); class A { public int $x; public ?int $y = null; public int $z = 42; public ?int $u; public ?datetime $v; public datetime $w; } $a = new A; var_dump($a->x); // Fatal error, uninitialized... var_dump($a->y); // null var_dump($a->z); // 42 var_dump(isset($a->z)); // true unset($a->z); var_dump(isset($a->z)); // false var_dump($a->z); // Fatal error, uninitialized... var_dump($a->u); // Fatal error, uninitialized... var_dump($a->v); // Fatal error, uninitialized... var_dump($a->w); // Fatal error, uninitialized... var_dump(isset($a->x)); // false var_dump(isset($a->y)); // false var_dump(isset($a->u)); // false var_dump(isset($a->v)); // false var_dump(isset($a->w)); // false Regards Thomas Tom Worster wrote on 26.05.2016 15:53:

Tom Worster

10 years ago
Hi Thomas, On the face of it, I'm not enthusiastic to introduce new magic numbers (which would be false, 0, 0.0, "", and [], right?) that PHP assigns when coercing a typed, uninitialized property read by a file in liberal mode. This is like taking the most confusing thing about 7.0's dual-mode, scalar type declaration of function arguments and boosting the confusion power. I would want a new name for this complement-of-strict mode. "Weak" and "liberal" don't quite do it. Promiscuous mode? ;) Tom On 5/26/16, 10:40 AM, "Thomas Bley" <mails@thomasbley.de> wrote:

Unnamed Person

10 years ago
it's not so magic, rather think of (int)null, (float)null, (string)null, (array)null, etc. Typed properties could be defined as nullable by default, but I think that makes userland code much more ugly. Regards Thomas Tom Worster wrote on 26.05.2016 18:44:

James Gilliland

10 years ago
> > If you want, you can easily write a backwards-compatible new class that > uses declared type properties with > public int $property = null;
And for those pesky third party libraries that forgot to set = NULL on their property definition there's a backwards compatible accessor too. $foo = $obj->bar ?? NULL; Sarcasm aside, I still can't figure out how fundamentally changing how people interact with uninitialized properties like this improves developer experience. Can someone explain a case where this is better and catches a bug or something? Since this is a new feature I would assume its not covered by "BC" but this seems like a painful gotcha for people developing across typed and untyped code.

Tom Worster

10 years ago
On 5/26/16, 11:02 AM, "James Gilliland" <neclimdul@gmail.com> wrote:
>Sarcasm aside, I still can't figure out how fundamentally changing how >people interact with uninitialized properties like this improves >developer experience. Can someone explain a case where this is better and >catches a bug or something? Since this is a new feature I would assume >its not covered by "BC" but this seems like a painful gotcha for people >developing across typed and untyped code.
Talk of improving developer experiences is too subjective for me. I only want to say that one option we have is to require that a PHP property with a type declaration must also have an initial value declaration. This obviates some confusion, which is a good thing in my opinion. I understand that it will be surprising to some that the lazy old `public $var;` (that initializes to null if it is read before written to) is not available if you insert a type declaration. Such surprise will dissipate quickly if the compiler rejects such cases, one way or the other: either don't declare type or declare type plus initial value. If I'm wrong in this estimation and we in fact need to protect developers from the pain of this experience then I'd prefer to reject typed properties for the time being. Tom

Fleshgrinder

10 years ago
The problem is a completely different one, how should the following code behave? class A { public int $x; } (new A)->x; The property has no value assigned but it is being accessed. The current PHP behavior is to simply initialize it with null. But this is impossible according to the type definition. There are not many ways to handle this. I think we already had all of them proposed: 0. Fatal error after __construct was called. 1. Fatal error and abort. 2. Initialize with appropriate type. 3. Initialize with null. Option 0. is out the window because it creates endless edge cases. Option 1. is extremely brutal and not necessarily what we want (lazy, anyone?). Option 2. has a huge problem with objects because it cannot initialize e.g. a \Fleshgrinder\Custom\SuperClass nor a \DateTime. Option 3. is the current behavior but silently doing so results in a type hint violation. Emitting an E_NOTICE at this point is the most sensible thing that we can do at this point in my opinion. Extending this logic to all kind of properties is just logical to keep conditionals in the internals low and have a consistent behavior across all of the userland functionality. After all, aren't the following things equal? $a; echo $a; // null + E_NOTICE class O { public int $x; } echo (new O)->x; // null + E_NOTICE One could even argue that an E_NOTICE is required for void routines too. function f() {} echo f(); // null + E_NOTICE But that's another story. :)
-- Richard "Fleshgrinder" Fussenegger

Tom Worster

10 years ago
On 5/26/16, 12:30 PM, "Fleshgrinder" <php@fleshgrinder.com> wrote:
>The problem is a completely different one, how should the following code >behave? > > class A { > > public int $x; > > } > > (new A)->x; > >The property has no value assigned but it is being accessed. The current >PHP behavior is to simply initialize it with null. But this is >impossible according to the type definition. > >There are not many ways to handle this. I think we already had all of >them proposed: > >0. Fatal error after __construct was called. >1. Fatal error and abort. >2. Initialize with appropriate type. >3. Initialize with null.
Under another 5th option, the problem you state does not arise. Disallow "public int $x;". Under this option you may declare $x with type int and an initial value or you may declare $x without type but you may not declare $x with type (nullable or not) and undefined initial value. Tom

Rowan Collins

10 years ago
On 26/05/2016 17:40, Tom Worster wrote:
>> 0. Fatal error after __construct was called. >> >1. Fatal error and abort. >> >2. Initialize with appropriate type. >> >3. Initialize with null. > Under another 5th option, the problem you state does not arise. Disallow > "public int $x;". Under this option you may declare $x with type int and > an initial value or you may declare $x without type but you may not > declare $x with type (nullable or not) and undefined initial value.
That has the same problem as 2 - not all types can be initialised statically, so can't be declared this way: class Foo { HTTPRequest $request = SOMETHING; } How do I safely initialise that? Regards,
-- Rowan Collins [IMSoP]

Fleshgrinder

10 years ago
On 5/26/2016 6:40 PM, Tom Worster wrote:
> On 5/26/16, 12:30 PM, "Fleshgrinder" <php@fleshgrinder.com> wrote: > >> The problem is a completely different one, how should the following code >> behave? >> >> class A { >> >> public int $x; >> >> } >> >> (new A)->x; >> >> The property has no value assigned but it is being accessed. The current >> PHP behavior is to simply initialize it with null. But this is >> impossible according to the type definition. >> >> There are not many ways to handle this. I think we already had all of >> them proposed: >> >> 0. Fatal error after __construct was called. >> 1. Fatal error and abort. >> 2. Initialize with appropriate type. >> 3. Initialize with null. > > Under another 5th option, the problem you state does not arise. Disallow > "public int $x;". Under this option you may declare $x with type int and > an initial value or you may declare $x without type but you may not > declare $x with type (nullable or not) and undefined initial value. > > Tom >
This would be a valid approach too, yes. I personally would be against it because I do not want to initialize all my properties. class A { private int $x; public function getX() { if (empty($this->x)) { $this->x = 42; } return $this->x; } } This would not yield an E_NOTICE because both isset() and empty() never do. This allows the attentive programmers to keep up there coding practices without the necessity to assign meaningless values everywhere. class A { /** -1 is invalid */ public int $x = -1; /** 'INVALID' is invalid but empty string is allowed */ public string $s = 'INVALID'; /** Null byte is invalid but anything else is valid */ public string $welcome_to_the_c_world = '\0'; } Not cool. :(
-- Richard "Fleshgrinder" Fussenegger

Tom Worster

10 years ago
On 5/26/16, 12:48 PM, "Fleshgrinder" <php@fleshgrinder.com> wrote:
>> >> Under another 5th option, the problem you state does not arise. Disallow >> "public int $x;". Under this option you may declare $x with type int and >> an initial value or you may declare $x without type but you may not >> declare $x with type (nullable or not) and undefined initial value. >> >> Tom >> > >This would be a valid approach too, yes. I personally would be against >it because I do not want to initialize all my properties. > > class A { > > private int $x; > > public function getX() { > if (empty($this->x)) { > $this->x = 42; > } > return $this->x; > } > > } > >This would not yield an E_NOTICE because both isset() and empty() never >do. This allows the attentive programmers to keep up there coding >practices without the necessity to assign meaningless values everywhere. > > class A { > > /** -1 is invalid */ > public int $x = -1; > > /** 'INVALID' is invalid but empty string is allowed */ > public string $s = 'INVALID'; > > /** Null byte is invalid but anything else is valid */ > public string $welcome_to_the_c_world = '\0'; > > }
If you want that kind of thing, you can do it the old PHP way like this class A { private ?int $x = null; ... Tom

Fleshgrinder

10 years ago
On 5/26/2016 7:00 PM, Tom Worster wrote:
> On 5/26/16, 12:48 PM, "Fleshgrinder" <php@fleshgrinder.com> wrote: >> This would be a valid approach too, yes. I personally would be against >> it because I do not want to initialize all my properties. >> >> class A { >> >> private int $x; >> >> public function getX() { >> if (empty($this->x)) { >> $this->x = 42; >> } >> return $this->x; >> } >> >> } >> >> This would not yield an E_NOTICE because both isset() and empty() never >> do. This allows the attentive programmers to keep up there coding >> practices without the necessity to assign meaningless values everywhere. >> >> class A { >> >> /** -1 is invalid */ >> public int $x = -1; >> >> /** 'INVALID' is invalid but empty string is allowed */ >> public string $s = 'INVALID'; >> >> /** Null byte is invalid but anything else is valid */ >> public string $welcome_to_the_c_world = '\0'; >> >> } > > If you want that kind of thing, you can do it the old PHP way like this > > class A { > private ?int $x = null; > ... >
Doing that defeats the whole purpose of typed properties in my opinion. I want to be sure that nobody can assign null to this property and only allow it to be null until I initialize it myself. class A { private int $x; private function getX() { if (empty($this->x)) { $this->x = 42; } return $this->x; } public function doSomething() { // lots of code ... $_ = $__ * $this->x; // lots of code ... } } This would emit an E_NOTICE (and possibly end up in an error due to null) and the developer of the doSomething() method will notice that she should call getX() instead. This would not be the case with private ?int $x. :(
-- Richard "Fleshgrinder" Fussenegger

Rowan Collins

10 years ago
On 26/05/2016 17:30, Fleshgrinder wrote:
> class O { > public int $x; > } > echo (new O)->x; // null + E_NOTICE
As I'm sure has already been pointed out (I haven't followed the whole thread) this defeats a large advantage of typed properties - I now can't read from the property without checking if it's null, so I can't do this: class O { public \DateTimeImmutable $d; } echo (new O)->d->format('Y-m-d H:i:s'); There's no ? on the type def, so I ought to be able to trust the type. A TypeError makes more sense here, because it is a programming error for it to be in this state. Regards, Rowan Collins [IMSoP]

Fleshgrinder

10 years ago
On 5/26/2016 6:50 PM, Rowan Collins wrote:
> As I'm sure has already been pointed out (I haven't followed the whole > thread) this defeats a large advantage of typed properties - I now can't > read from the property without checking if it's null, so I can't do this: > > class O { > public \DateTimeImmutable $d; > } > echo (new O)->d->format('Y-m-d H:i:s'); > > There's no ? on the type def, so I ought to be able to trust the type. A > TypeError makes more sense here, because it is a programming error for > it to be in this state. >
That is exactly what Stanislav raised too. I really have no idea how we should prevent this from happening and honestly think we shouldn't. Your construct will result in a fatal error (also known as NullPointerException to some, *evil-laughter*) anyways preceded by an E_NOTICE that tells you that your property is not defined. I am of course open for ideas but emitting another kind of fatal error does not really make things better imho.
-- Richard "Fleshgrinder" Fussenegger

Rowan Collins

10 years ago
On 26/05/2016 17:57, Fleshgrinder wrote:
> On 5/26/2016 6:50 PM, Rowan Collins wrote: >> As I'm sure has already been pointed out (I haven't followed the whole >> thread) this defeats a large advantage of typed properties - I now can't >> read from the property without checking if it's null, so I can't do this: >> >> class O { >> public \DateTimeImmutable $d; >> } >> echo (new O)->d->format('Y-m-d H:i:s'); >> >> There's no ? on the type def, so I ought to be able to trust the type. A >> TypeError makes more sense here, because it is a programming error for >> it to be in this state. >> > > That is exactly what Stanislav raised too. I really have no idea how we > should prevent this from happening and honestly think we shouldn't. Your > construct will result in a fatal error (also known as > NullPointerException to some, *evil-laughter*) anyways preceded by an > E_NOTICE that tells you that your property is not defined. > > I am of course open for ideas but emitting another kind of fatal error > does not really make things better imho.
I think the difference is the emphasis of whose responsibility it is to fix it: a TypeError confirms that the error is in the O class for exposing an incorrectly typed property; a NullPointerException, as you put it, makes it my fault for trusting the class. Or to put it a different way, is the error in the first arrow (accessing "->d") or the second one (de-referencing "d->"). At the end of the day, all the type notations being added to PHP are just assertions anyway. So the same could be said of this: function foo(\DateTime $d) { echo $d->format('Y-m-d H:i:s'); } foo(null); If this didn't throw an error at "foo(null)", it would throw an error at "$d->format". Regards,
-- Rowan Collins [IMSoP]

Fleshgrinder

10 years ago
On 5/26/2016 7:20 PM, Rowan Collins wrote:
> I think the difference is the emphasis of whose responsibility it is to > fix it: a TypeError confirms that the error is in the O class for > exposing an incorrectly typed property; a NullPointerException, as you > put it, makes it my fault for trusting the class. > > Or to put it a different way, is the error in the first arrow (accessing > "->d") or the second one (de-referencing "d->"). > > > At the end of the day, all the type notations being added to PHP are > just assertions anyway. So the same could be said of this: > > function foo(\DateTime $d) { > echo $d->format('Y-m-d H:i:s'); > } > foo(null); > > If this didn't throw an error at "foo(null)", it would throw an error at > "$d->format". >
Yes, they are just assertions and design by contract and you make a very good point here for an actual error. I am convinced. ;) However, it should not throw an error for isset() and empty() to allow more special constructs. As we already have it in place everywhere with the two.
-- Richard "Fleshgrinder" Fussenegger

Lester Caine

10 years ago
On 26/05/16 18:26, Fleshgrinder wrote:
> On 5/26/2016 7:20 PM, Rowan Collins wrote: >> > I think the difference is the emphasis of whose responsibility it is to >> > fix it: a TypeError confirms that the error is in the O class for >> > exposing an incorrectly typed property; a NullPointerException, as you >> > put it, makes it my fault for trusting the class. >> > >> > Or to put it a different way, is the error in the first arrow (accessing >> > "->d") or the second one (de-referencing "d->"). >> > >> > >> > At the end of the day, all the type notations being added to PHP are >> > just assertions anyway. So the same could be said of this: >> > >> > function foo(\DateTime $d) { >> > echo $d->format('Y-m-d H:i:s'); >> > } >> > foo(null); >> > >> > If this didn't throw an error at "foo(null)", it would throw an error at >> > "$d->format". >> > > Yes, they are just assertions and design by contract and you make a very > good point here for an actual error. I am convinced. ;) > > However, it should not throw an error for isset() and empty() to allow > more special constructs. As we already have it in place everywhere with > the two.
DateTime is probably a better example to work with than 'int' since it already has a long pedigree of problems and clashes where many projects had their own interpretation and still use their own version of the class. My problem with '\DateTime $d' is that by default it returns 'now' rather than an 'empty/null' value in which to put the data from the database. I can't create a 'null' version of DateTime so one has to have a phantom date simply to ensure you know if it has been initialized with your own data later - or continue to use the older user space classes. It is handling just how the variable/property is initialized that is the whole problem here and there may be very good reasons that the initialization is later then the 'construct' be that a DateTime or an int ... null is the correct way of indicating that 'as yet we do not have the value of type xxx' just as it does for an untyped value, so trying to make a special case to block it's use is simply strangling normal usage in other cases?
-- Lester Caine - G8HFL ----------------------------- Contact - http://lsces.co.uk/wiki/?page=contact L.S.Caine Electronic Services - http://lsces.co.uk EnquirySolve - http://enquirysolve.com/ Model Engineers Digital Workshop - http://medw.co.uk Rainbow Digital Media - http://rainbowdigitalmedia.co.uk