Hi folks.
There have been a lot of RFCs and possible RFCs of late that are all circling around the same related problem space: Working with objects right now involves too much boilerplate to get things done. As I've mentioned several times, I believe we need to be looking for broader solutions rather than narrowly-focused one-offs.
To that end, I have written an extensive analysis of the problem space and the current and recent proposals. I've put it on my blog rather than inline here because it's quite long and the blog offers better formatting.
Discussion can happen here, but I'll also respond to comments there.
In short: I believe our biggest potential win is to focus on 3 RFCs:
- Constructor Promotion
- Named parameters
- Compound Property Visibility
For details, see the full writeup:
https://hive.blog/php/@crell/improving-php-s-object-ergonomics
Thank you for your attention.
--
Larry Garfield
larry@garfieldtech.com
In short: I believe our biggest potential win is to focus on 3 RFCs:
- Constructor Promotion
I would vote yes on this, assuming the implementation is sane.
- Named parameters
This one is tricky -- there are tradeoffs of every approach I've seen
proposed. I have an idea for another way to address this space, but it
too has drawbacks; there's inherently no free lunch here, I think.
- Compound Property Visibility
What is this last one? I searched for "compound" in the article, but
did not get many hits, none of which described what it actually is.
Merging some replies together here...
In short: I believe our biggest potential win is to focus on 3 RFCs:
- Constructor Promotion
I would vote yes on this, assuming the implementation is sane.
That still doesn't resolve issue with lot of boilerplate when you deal with
small objects for which public readonly is enough like object-initializer
could. So I'm not sure about my vote here. It does solve only one narrow
situation for me.
The value here is combining constructor promotion with named parameters. Constructor promotion itself is useful for the class implementer, but doesn't help the caller. Named parameters helps the caller, but doesn't really help the class implementer. The combination of both of them together gives us something similar to the object-initializer syntax as a net result, but with many other benefits because it's not a one-off syntax.
So with the two of them together, you get:
class Point {
public function __construct({public int $x, public int $y});
}
Which then allows any of these construction mechanisms:
$p1 = new Point(5, 7);
$p2 = new Point({x: 5, y: 7});
$p3 = new Point({y: 7, x: 5});
All of which result in an object you can use the same way:
print $p1->x . ', '. $p1->y;
- Named parameters
This one is tricky -- there are tradeoffs of every approach I've seen
proposed. I have an idea for another way to address this space, but it
too has drawbacks; there's inherently no free lunch here, I think.
Please share. I make no claim of being fully versed in the implementation details of how these would work at the engine level, only at the syntax level.
- Compound Property Visibility
What is this last one? I searched for "compound" in the article, but
did not get many hits, none of which described what it actually is.
Mm, yeah, I sorta changed names on that part way through. That's the "different visibility for read and for write" proposal that Nicolas mentioned in the readonly thread, for which I borrowed the syntax from the property accessors RFC.
- Your post mentions validation needed for value objects — a concern I
share — but AFAICT your conclusion does not address the issue.
It doesn't directly in a syntactic way. Rather, it allows the constructor and Setter/Wither methods to still exist as now, and users can write what they want in there. That is, it's no change from now. That was one of my evaluation criteria: Improve as many of these as possible without making any of them worse.. This is one were "nothing is made worse".
- It also mentions overlapping concerns and references numerous RFCs,
but there were two other relative works not mentioned. One[1] is an RFC
and the other[2] a PR on Github. I think they both overlaps with this
problem space, with the former addressing validation whereas the latter
case potentially conflicts with constructor promotion.
You are correct, I forgot about the impact of annotations (which I also support). Off hand, I think their only issue would be in relation to the constructor promotion, as the resulting syntax to put a bunch of annotations inside the constructor definition would get... silly.
That suggest we may want a different syntax for constructor promotion than what I proposed we borrow from Hack, but I don't think changes the overall argument.
- You don't really address the value object vs. service object
distinction, except on the periphery.
Correct. This also ties into your discussion of Structs/Records as an alternative approach. I have considered that before, but in this writeup... I found nowhere that any of the possible implementations would be useful only for one or the other. Rather, certain features may be more useful to one or the other but would definitely have uses in both.
At the end of the day, I think the only language-level difference between a value and service object is the passing semantics; service objects should pass as they do now, while value objects would, ideally, pass in a way more similar to arrays. However, that was off topic at this time, and not needed. Everything else discussed would be applicable to both object types, so splitting the syntax would not be helpful, just confusing.
For instance, property accessor methods on a new Struct type would have the exact same performance issue as they would on a Class today.
The goal was to focus on the minimum amount of work we can do to get the maximum benefit, and I don't think a new Struct type would qualify for that.
- You mention the concerns about exposing parameter names as part of
the API but don't address those concerns directly.
I do. Specifically, making named parameters opt-in allows class/function/method authors to decide if they want to make their parameter names part of the API or not. The alternative to support it on all callables period, which is also a viable option but, as noted in the writeup, technically a BC change. Either approach is possible.
Now that I think about it, though, named parameters and variadics may not play nicely at all, so we may want to force it to be opt-in only (and incompatible with variadics).
- You mention get/set properties surprisingly running code but do not
address those concerns directly.
That's only an issue with full property accessors, which I specifically do not propose we implement at this time. I was trying to represent the arguments against property accessors last time, as I remembered them; I personally don't think it's a problem and would love to have property accessors exactly as described in the RFC, if the performance issues could be resolved.
Since that's not what is being proposed, determining if that's even an issue is not within scope.
- Your concept for a JSON object-like syntax for passing parameter
feels incomplete to me. Unless I miss understand it is just a special
syntax that only works in the context of passing arguments to a
constructor, method or function, and not a first-class language
element. If we were to go that route I think we would find it highly
limiting.
The JSON-esque syntax was one of the options previously discussed the last time named parameters came up. I am not wedded to it, it was just the most readily-available way to demonstrate named params being opt-in. If we make them not opt-in, then there is no need for a special syntax at all.
I was very specifically NOT trying to propose a Javascript-like Object Literal syntax. It's just one option among many for denoting a named parameters call. So, yes, it only works in the context of passing arguments, by design, that's the point.
- In the section you say that "either of the following construction
styles becomes possible" but you do not talk about the option of having
one of more of the first parameters being positional and the rest being
able to be passed by name, which I think would be an ideal use-case
when you want to force certain parameters to always be passed but make
the rest optional.
Incorrect. I specifically say in one of the notes that we could consider it, but it's not necessary and may be more trouble than it's worth. It's more in-the-weeds than I wanted to get at this stage.
Specifically:
"We can consider mixing positional and named parameters the way Python does, but I don't think that's necessary."
Regarding interaction with Delegation (a la Go, which I also agree is a very nice feature and we wants it, precious), I'm not sure. I would have to defer to Nikita for how that would interact with constructor promotion. Between that and annotations we may well want to explore a different syntax than Hack, but the essential concept is the same.
- Going further with constructor promotion, which you join with named
parameters, how are the parameters represented inside the
constructor/method/function? As different variables methods just like
current parameters? Is there no concept of automatic aggregation of
those parameters into some kind of structure that could then be passed
down to other methods and functions?
Yes there is; it's called $this. The whole point of constructor promotion is that it avoids you writing
$this->a = $a;
$this->b = $b;
$this->c = $c;
One possible implementation in fact is to simply auto-generate those exact op codes in the source as it's getting parsed. If you want to then add additional logic in the constructor body that would run after those assignments, cool, those still work, and the corresponding properties are already populated for you to use if needed.
--Larry Garfield
Hi Larry,
pon., 23 mar 2020 o 20:04 Larry Garfield larry@garfieldtech.com
napisał(a):
Merging some replies together here...
In short: I believe our biggest potential win is to focus on 3 RFCs:
- Constructor Promotion
I would vote yes on this, assuming the implementation is sane.That still doesn't resolve issue with lot of boilerplate when you deal
with
small objects for which public readonly is enough like object-initializer
could. So I'm not sure about my vote here. It does solve only one narrow
situation for me.The value here is combining constructor promotion with named parameters.
Constructor promotion itself is useful for the class implementer, but
doesn't help the caller. Named parameters helps the caller, but doesn't
really help the class implementer. The combination of both of them
together gives us something similar to the object-initializer syntax as a
net result, but with many other benefits because it's not a one-off syntax.So with the two of them together, you get:
class Point {
public function __construct({public int $x, public int $y});
}Which then allows any of these construction mechanisms:
$p1 = new Point(5, 7);
$p2 = new Point({x: 5, y: 7});
$p3 = new Point({y: 7, x: 5});All of which result in an object you can use the same way:
print $p1->x . ', '. $p1->y;
I agree it looks a little bit awkward and differs from object-initializers
known from other languages,
but let's say it would work somehow for this example. Now make it not 2 but
10-15 properties
with real types sometimes quite long so after 3-5 of them you should break
the line,
then add some default values.
Like a real entity which with typed properties doesn't need setters and
getters.
The example grows but even when breaking a line after each
parameter/property still could be somehow readable.
Now as we deal with Entity add some annotations or let's go hype, try with
new Attributes v2
proposed by Benjamin Eberlei
https://wiki.php.net/rfc/attributes_v2#userland_use-casemigrating_doctrine_annotations_from_docblocks_to_attributes
3 for $id and for the rest at least one attribute per property.
class Product {
public function __construct({
<<ORM\Id>>
<<ORM\Column>>
<<ORM\GeneratedValue>>
public int $id,
<<ORM\Column(["unique" => true])>>
public string $name,
<<ORM\Column>>
public string $description
});
}
Let's stop on 3 I think it's enough to see it's:
- unusual to see annotations in method signature declaration
- not readable anymore.
Now if you say it shouldn't be like that and all the properties should be
declared as normal properties,
then the constructor is not needed to simplify the declaration but still
requires a lot of boilerplate on
the caller side undoubtedly.
Do you still think object-initializer is pointless and useless and can be
replaced with
named arguments and constructor arguments promotion?
Cheers,
Michał
Hi Larry,
The value here is combining constructor promotion with named parameters.
Constructor promotion itself is useful for the class implementer, but
doesn't help the caller. Named parameters helps the caller, but doesn't
really help the class implementer. The combination of both of them
together gives us something similar to the object-initializer syntax as a
net result, but with many other benefits because it's not a one-off syntax.So with the two of them together, you get:
class Point {
public function __construct({public int $x, public int $y});
}Which then allows any of these construction mechanisms:
$p1 = new Point(5, 7);
$p2 = new Point({x: 5, y: 7});
$p3 = new Point({y: 7, x: 5});All of which result in an object you can use the same way:
print $p1->x . ', '. $p1->y;
I agree it looks a little bit awkward and differs from object-initializers
known from other languages,
but let's say it would work somehow for this example. Now make it not 2 but
10-15 properties
with real types sometimes quite long so after 3-5 of them you should break
the line,
then add some default values.
Like a real entity which with typed properties doesn't need setters and
getters.The example grows but even when breaking a line after each
parameter/property still could be somehow readable.Now as we deal with Entity add some annotations or let's go hype, try
with
new Attributes v2
proposed by Benjamin Eberlei
https://wiki.php.net/rfc/attributes_v2#userland_use-casemigrating_doctrine_annotations_from_docblocks_to_attributes
3 for $id and for the rest at least one attribute per property.class Product {
public function __construct({
<<ORM\Id>>
<<ORM\Column>>
<<ORM\GeneratedValue>>
public int $id,<<ORM\Column(["unique" => true])>> public string $name, <<ORM\Column>> public string $description });
}
Let's stop on 3 I think it's enough to see it's:
- unusual to see annotations in method signature declaration
- not readable anymore.
Now if you say it shouldn't be like that and all the properties should be
declared as normal properties,
then the constructor is not needed to simplify the declaration but still
requires a lot of boilerplate on
the caller side undoubtedly.Do you still think object-initializer is pointless and useless and can be
replaced with
named arguments and constructor arguments promotion?Cheers,
Michał
Yes, I responded to the annotations point in my earlier reply. I hadn't considered those, so we'll likely need to consider alternate syntaxes if we want both annotations and constructor promotion (which I do). However, note that even the (admittedly fugly) Product example you have above, each property name is listed only once, not 4 times, making it still an improvement on the status quo as far as redundancy.
However, I still hold that constructor promotion and named parameters, taken together, are strictly superior to a one-off syntax for objects that only supports public properties, as they offer more capabilities and have less potential to be confused with future additions.
Eg, object-initalizer syntax vs named parameters on a constructor; which to use when? Why? Why do both exist? They really shouldn't. Named params would be better than object-initalizers all on their own, regardless of whether constructor promotion is included, precisely because they offer more capability for the same syntactic addition.
--Larry Garfield
Hi Larry,
In my opinion, one of the core assets of PHP is that it contains relatively
few syntactic sugar compared
to some other languages, e.g. C#. Maybe it's just me, but I believe it
makes the code written in PHP
easier to read. And I think this is what we should optimize for. Not for
saving a few lines of code,
but for making the code easier to read and understand.
Now, if we had constructor promotion, we should search for properties both
on the top of the class
and in the constructor. And I agree with Michał, this kind of code can get
out of hand of control very fast.
That said, I don't think that declaring properties in the constructor is a
good idea. It's also because
many people (including myself) tend to write static methods first (I mainly
use them as named constructors),
so we'd either lose track of properties declared in the constructor or have
to force a code style that
puts the constructor to the top. Also, some IDEs (but PHPStorm for sure)
can generate the constructor
very easily from the declared properties.
Speaking about the evaluation of "Write-Once Properties" and "Compound
Property Visibility",
I disagree in some regards. I'll start with the less important one:
Because the write-once state is preserved across cloning, it makes
Evolution worse.
I think it's quite expected that properties won't be writable after
cloning. That would be a very bad
design otherwise. That's why I think your real problem is the opposite:
that currently the clone operator
is not prepared for this change. That's why I missed the "Rust-like
cloning" (or the other clone variant
that I presented in the "write-once property" thread) as the solution of
the "Evolution" problem of
"write-once" properties.
My other problem with the evaluation of the "Immutability problem" is that
it suggests that immutability
is only an external concern, and it isn't a thing in the private/protected
scope. Why do you think so?
Currently (unfortunately) visibility is the only thing that can at some
extent (in external scopes) control
mutability in PHP. However, if we look at the problem from the type system
perspective, visibility has nothing
to do with it: we won't have any guarantee that a property is immutable
even if we make it private.
To be honest, my impression is that most of the problems you list (e.g.
verbose constructor, bean problem,
or even property accessors) mainly boil down to the verbosity of PHP (or
the "visual debt" problem
how some people calls it). As I wrote in the first paragraph, I don't think
it's a bad thing.
For example, if we had "compound property visibility" then we could
separate read/write visibility of properties
without using getters/setters (I think this is what you also wrote). If we
had property accessors then
besides the separation of visibility, we could have materalized properties
or properties that validate themselves.
Probably the syntax would become more concise, but effectively we would
make methods from the properties.
But I don't understand why is would be a good thing to have two types of
methods? How should we decide if we
should use normal methods or property accessors? I think the current
situation is much better: use getters
and/or setters to separate visibility of properties, and perform validation
in setters if you need it. And I
I still don't think that property accessors would solve the main use-case
of "write-once" properties.
Cheers,
Máté
Hi Larry,
In my opinion, one of the core assets of PHP is that it contains relatively
few syntactic sugar compared
to some other languages, e.g. C#. Maybe it's just me, but I believe it
makes the code written in PHP
easier to read. And I think this is what we should optimize for. Not for
saving a few lines of code,
but for making the code easier to read and understand.
It's not just you. :) I fully agree with you here. PHP has a straight,
down-to-earth character with very little magic going on. This is what
makes the code easy to understand and debug.
I just would not use the term 'syntactic sugar' in this context. To me,
syntactic sugar has a positive connotation: A nicer way to express the
same thing that is easier to read. Magic is making things happen in
non-obvious ways that are easily missed when looking at the code. Magic
can look really attractive, until something does not work as expected
and you fail to see why. Python code tends to have tons of it.
The point where sugar ends and magic begins is a matter of taste. To me,
constructor promotion tends slightly towards magic. But then, anyone can
choose to use the feature or not.
To be honest, my impression is that most of the problems you list (e.g.
verbose constructor, bean problem,
or even property accessors) mainly boil down to the verbosity of PHP (or
the "visual debt" problem
how some people calls it). As I wrote in the first paragraph, I don't think
it's a bad thing.
Reducing verbosity is not the problem. Introducing magic is.
While property accessors are magic, they are a form of magic that I
would be willing to accept, for the following reasons.
A nice feature of accessors is that they allow swapping a traditional
public class property for a property accessor without changing API. Such
a feature currently has little value, because public properties are not
commonly used because they cannot be marked as read-only yet. Once
public class properties can be marked read-only I would be comfortable
exposing them directly, without writing any getters and setters for
them. The only thing that would still worry me is: What if I need to add
some form of access logic later on? Property accessors allow me to do
this without introducing a BC break.
So yes, property accessors are magic. However, in combination with
read-only properties they would allow for dropping tons of getter
methods and directly expose properties in stead. They allow this because
they are the 'safety net' which makes me comfortable doing it. We get
more PHP and less Java.
This probably means that I will occasionally have to actually use
property accessors at some point and introduce a bit of magic. Then, it
still isn't the worst possible magic. Accessors are explicitly declared
on the class, an editor could easily show me that accessing a particular
property calls a method and show me the code. It's not ideal but it's
not that bad either.
But I don't understand why is would be a good thing to have two types of
methods? How should we decide if we
should use normal methods or property accessors? I think the current
I would prefer to only use accessors to add logic to property access
without introducing a BC break. A successor to using __get() and
__set(). For access logic that is obviously non-trivial from the start I
would probably continue to use regular getter and setter methods.
I still don't think that property accessors would solve the main use-case
of "write-once" properties.
Indeed. For that purpose I would prefer a keyword for marking a public
property as read-only.
Regards,
Dik Takken
Hi Larry,
pon., 23 mar 2020, 01:48 użytkownik Larry Garfield larry@garfieldtech.com
napisał:
Hi folks.
There have been a lot of RFCs and possible RFCs of late that are all
circling around the same related problem space: Working with objects right
now involves too much boilerplate to get things done. As I've mentioned
several times, I believe we need to be looking for broader solutions rather
than narrowly-focused one-offs.To that end, I have written an extensive analysis of the problem space and
the current and recent proposals. I've put it on my blog rather than
inline here because it's quite long and the blog offers better formatting.Discussion can happen here, but I'll also respond to comments there.
In short: I believe our biggest potential win is to focus on 3 RFCs:
- Constructor Promotion
That still doesn't resolve issue with lot of boilerplate when you deal with
small objects for which public readonly is enough like object-initializer
could. So I'm not sure about my vote here. It does solve only one narrow
situation for me.
- Named parameters
What would be nice for methods and functions. +1
- Compound Property Visibility
I didn't get it what benefits over property accessors it could have.
For details, see the full writeup:
https://hive.blog/php/@crell/improving-php-s-object-ergonomics
Thank you for your attention.
--
Larry Garfield
larry@garfieldtech.com
https://hive.blog/php/@crell/improving-php-s-object-ergonomics
Hi Larry,
That is a really excellent writeup. Thanks from me at least for taking the time to write it up in depth.
Looking at your conclusion, my gut feelings tell me your conclusion is about 85% there, but with about 15% that gives me pause unless addressed, which I think is imminently possible.
I'm going to do my best to explain what I am envisioning but realize I had not reserved time for this today so I am rushing to complete before my Monday starts in too few hours. Given that I will almost certainly not be as clear in my thoughts had I had time to write and review it so please ask for clarification if something does not make sense.
-
Your post mentions validation needed for value objects — a concern I share — but AFAICT your conclusion does not address the issue.
-
It also mentions overlapping concerns and references numerous RFCs, but there were two other relative works not mentioned. One[1] is an RFC and the other[2] a PR on Github. I think they both overlaps with this problem space, with the former addressing validation whereas the latter case potentially conflicts with constructor promotion.
-
And finally, some of the proposed concepts add syntax elements but do not make them first class language elements.
But before I cover those let me cover what I think you nailed to perfection:
- The need resolve the verbosity of day-to-day programming with classes on PHP.
- The distinction between Service and Value objects.
- The value of immutability.
- The downside of using arrays to pass properties (which is how many of us do it today.)
- The benefits of contextual access
- The limited nature of COPA/and write-once properties.
- The elegance of property accessor syntax and how it can be applied to create readonly properties
- The elegance of named parameters with the distinction between BC parameter passing and a newer JSON object-like syntax.
- Rust-like cloning/Construct from syntax.
- The problem of exposing parameter names as part of an external API for existing code
- The problems associated with get and set properties actually running code when not expected by the user.
However, here is were I think your proposal still needs tightening up.
-
You don't really address the value object vs. service object distinction, except on the periphery.
-
You mention the concerns about exposing parameter names as part of the API but don't address those concerns directly.
-
You mention get/set properties surprisingly running code but do not address those concerns directly.
-
Your concept for a JSON object-like syntax for passing parameter feels incomplete to me. Unless I miss understand it is just a special syntax that only works in the context of passing arguments to a constructor, method or function, and not a first-class language element. If we were to go that route I think we would find it highly limiting.
-
In the section you say that "either of the following construction styles becomes possible" but you do not talk about the option of having one of more of the first parameters being positional and the rest being able to be passed by name, which I think would be an ideal use-case when you want to force certain parameters to always be passed but make the rest optional.
- Let me now address the conflict with Nikita's "decorator" pattern support PR (which I and several others think would be better called delegation.) (As an aside, Go has such a feature and after having used it I find working with PHP's lack of delegation to be extremely painful. Well at least we have Traits. You can see my comment on that PR and my proposal here[3] and [4].)
I think that delegation addresses the no-win scenario of and thus is an extremely important addition not to block:
- "Should I extend a class and have fragile base classes?"
- "Should I use containment and then have a nightmare of boilerplate?", or
- "Should I use magic methods and loose performance and native ability use reflection and *_exists() functions?"
Unfortunately I think that Constructor Promotion assumes that all properties will be associated with the class at hand, and not with classes that are being delegated to. Yes we could assume they are joined together, and distributed to the various delegated instances, but I believe that could get very complicated very quickly.
-
Going further with constructor promotion, which you join with named parameters, how are the parameters represented inside the constructor/method/function? As different variables methods just like current parameters? Is there no concept of automatic aggregation of those parameters into some kind of structure that could then be passed down to other methods and functions?
-
And — I don't want to bike-shed — but I think the proposed constructor promotion syntax could result in potentially high levels of visual complexity that all must be syntactically correct across many different lines. That has the making of some very fragile code and code that is rather hard to follow, much like when developers write "metadata driven" code where they instantiate an array that contains many other arrays and the entire structure traverses several hundred lines of code or more.
One key language addition solves many of the above problems.
Imagine that we add a "structs" to PHP. As I envision it:
A struct is a value object that has properties but either no methods or limited methods.
Structs don't need to be backward compatibility except not to conflict with existing syntax.
Structs are as simple as this (public is assumed for properties in my examples, unless otherwise specified):
struct Person {
string $firstName
string $lastName
}
Structs don't have constructors but do have an initializer syntax so Structs would be created like this:
$person = Person{firstName: 'Mike', lastName: 'Schinkel' }
This of a struct as a lightweight object with a few non-BC rules (like pass-by-value, more on that below).
Struct properties could be accessed just like object properties, with the thin arrow ("->"):
echo $person->lastName;
Structs could potentially have get/set so as to allow read-only or write only properties:
struct Person {
// This is read only
private string $firstName {
public get();
private set();
}
// This is write only
private string $lastName {
private get();
public set();
}
}
Structs could possibly also support "Materialized Values" assuming those accessors were limited to only accessing other properties (but that might be really hard to implement):
struct Person {
public string $firstName;
public string $lastName;
private string $fullName {
public get(){
return sprint('%s %s', $this->firstName, $this->lastName );
}
}
}
Structs could also be extended via classes.
class Developer extends Person {
function work() {
// writes code
}
}
Even better classes could delegate to Structs:
class Developer {
use struct Person;
}
And we could use aliases
class Developer {
use struct Person as person;
}
And we could disambiguate
class DevelopmentManager {
use class Manager;
use class Developer {
work as writeCode;
}
}
Further, we could support define named parameters by defining a Struct. Combine that with Construct-from syntax and it rid of the multi-line complexity and mess of the mess of Constructor Promotion:
namespace Structs;
struct Person {
public string $firstName;
public string $lastName;
}
namespace BizObjects;
class Person{
use struct Structs\Person;
}
Better, we could allow sharing of names between a struct and a class (if in the same file) which could associate them:
struct Person {
public string $firstName;
public string $lastName;
}
class Person{
use struct;
}
Which could be equivalent to:
class Person{
use struct {
public string $firstName;
public string $lastName;
}
}
Given the above the following function would only accept the object Person:
function renderPersonCard( Person $person ) {
/// rendering...
}
This however could access the struct person:
function renderPersonCard( Person::struct $person ) {
/// rendering...
}
With the above class/struct Person, you might create like this:
$person = new Person(Person{firstName: 'Mike', lastName: 'Schinkel' });
Or the shorthand:
$person = new Person({firstName: 'Mike', lastName: 'Schinkel' });
The above needs no explicit constructor, but what if we need to provide one? Note this example automatically initializes firstName and lastName:
class Person{
private static $_people = array();
use struct {
public string $firstName;
public string $lastName;
}
function __construct(Person::struct) {
self::$_people[] = $this;
}
}
Then there is the concern about parameters that are always required. They could be handled like this implicitly:
class Person{
private static $_people = array();
use struct {
public int $personId;
public string $firstName;
public string $lastName;
}
function __construct(int $personId, Person::struct);
}
Or explicitly:
class Person{
use struct {
public int $personId;
public string $firstName;
public string $lastName;
}
function __construct(int $personId, Person::struct) {
$this->personId = $personId;
}
}
We could even move $personId out of the struct and into the class, if we wanted to:
class Person{
public int $personId;
use struct {
public string $firstName;
public string $lastName;
}
function __construct(int $personId, Person::struct) {
$this->personId = $personId;
}
}
But what about our development manager? Note that a class would have an automatic Struct which would just be its properties:
class DevelopmentManager {
use class Manager;
use class Developer {
work as writeCode;
}
function __construct(Manager::struct, Developer::struct) {
self::$_people[] = $this;
}
}
The above would require instantiation like this:
$person = new DevelopmentManager(
Manager{firstName: 'Mike', lastName: 'Schinkel' },
Developer{ide:"PhpStorm", languages:["PHP","Go","SQL","etc.]}
);
Which could be shortened to:
$person = new DevelopmentManager(
{firstName: 'Mike', lastName: 'Schinkel' },
{ide:"PhpStorm", languages:["PHP","Go","SQL","etc.]}
);
Or with this construct:
class DevelopmentManager {
use class Manager;
use class Developer {
work as writeCode;
}
function __construct(Manager::struct + Developer::struct) {
self::$_people[] = $this;
}
}
Could be shortened further to:
$person = new DevelopmentManager({
firstName: 'Mike',
lastName: 'Schinkel',
ide:"PhpStorm",
languages:["PHP","Go","SQL","etc."],
});
Structs could also support immutability by having them passed to methods and functions by value — like arrays — instead of by reference like objects.
In addition, if we embrace annotations we can address validation (note I'm using 'attribute(s)' keyword because <<>> created too much visual noise and annotation is longer IMO):
class Person{
use struct {
attributes NotEmpty, ProperCase
public string $firstName;
attributes NotEmpty, ProperCase
public string $lastName;
attribute ValidEmail
public string $email;
attributes CanBeEmpty, ValidPhone
public string $phone;
}
}
Notice how the inclusion if Structs solve so many of the edge cases of the original proposal, and provide significant new capabilities in a very elegant manner? It addresses:
- Value objects and possibly Materialized values
- Need for backward compatibility with objects but not with structs
- Unifying value objects with classes
- By-value passing supports immutability
- Delegation is not blocked
- The "Named parameters" syntax is directly supported because it becomes the syntax to create struct instances.
- It leverages Construct-from syntax
- It reduces repeated references without forcing their declarations between contractor/method/function parentheses
- Supports working with attributes/annotations better than Constructor promotion
- And it is fully compatible with Get/Set property accessors.
So there you have it. I probably missed something and I probably was not clear enough in some area. Please do me the favor and if you have a question about how something will work first try to envision how it could work so you can suggest that when replying.
Looking forward to your thoughts.
-Mike
P.S. Unfortunately with all the demand lately they seem to have run out of flame-retardant suits so I present this with no such protection.
[1] https://wiki.php.net/rfc/annotations_v2
[2] https://github.com/php/php-src/pull/5168
[3] https://github.com/php/php-src/pull/5168#issuecomment-586688715
[4] https://mikeschinkel.me/2020/adding-delegation-to-php/#summary
On Mon, Mar 23, 2020 at 1:48 AM Larry Garfield larry@garfieldtech.com
wrote:
Hi folks.
There have been a lot of RFCs and possible RFCs of late that are all
circling around the same related problem space: Working with objects right
now involves too much boilerplate to get things done. As I've mentioned
several times, I believe we need to be looking for broader solutions rather
than narrowly-focused one-offs.To that end, I have written an extensive analysis of the problem space and
the current and recent proposals. I've put it on my blog rather than
inline here because it's quite long and the blog offers better formatting.Discussion can happen here, but I'll also respond to comments there.
In short: I believe our biggest potential win is to focus on 3 RFCs:
- Constructor Promotion
- Named parameters
- Compound Property Visibility
For details, see the full writeup:
https://hive.blog/php/@crell/improving-php-s-object-ergonomics
Thank you for your attention.
Thanks for the write-up Larry. I like where you're going with this.
If we were starting from a blank slate design, I would advocate for:
a) having an object initializer syntax
b) not having first-class constructors at all
c) using named constructors instead.
This also happens to be exactly what Rust does, go figure... Unfortunately
this kind of approach is hard to retrofit into PHP, because we already have
constructors, almost all classes define them, and it's hard to reconcile
object initialization syntax and non-trivial constructors in a meaningful
way. Combining this with the "no public properties" cargo cult we have
inherited from Java, the paradigm shift is probably too large here.
If we can't have object initializers, then improving what we can do with
constructors is the next best thing :)
I generally like the ideal of combining property declaration and
constructors. I've had this on my mind for a while already, and also
received the same suggestion from a couple of other people (I think Nicolas
was one of them?) The current amount of boilerplate that is needed is just
large enough that I will often go with a quick and simple ad-hoc array
structure rather than declaring an explicit value object type. The main
concern, as others have already mentioned, is that these inline
declarations can end up being quite verbose, especially once attributes get
involved.
I think I will write up a quick implementation & RFC for this part, as it
seems like something we should at least consider in more detail.
Named parameters are a pretty tough topic. I think one of the main points
of contention is that they make the parameters names part of the API
contract, and as such also subject to LSP. Your proposal offers two
possible ways to side-step this: First, by making named parameters opt-in
with a special syntax {}. Second, by limiting them to constructors. The
latter variant still exposes parameter names in the API, but at least does
not require their preservation across inheritance, as constructors are
excluded from LSP. I'm somewhat torn on this, because it makes named
parameters unusable with the very large body of existing methods, and
introduces an inconsistency in which methods can use named params and which
don't.
Regarding the remainder, I think that all of readonly properties,
asymmetric visibility and property accessors have their place and value,
with some overlap between them. As you already mentioned, the previous
property accessors proposal also included asymettric visibility as a
special case, and that's how I would introduce it as well.
However, I generally think that the main value really is the readonly
properties as proposed in the recent RFC. Nowadays, a large fraction of the
classes I use are immutable value objects, for which public readonly
properties provide a much closer match to the semantics I want.
I think that the problem with with-er methods is just that: It's a problem
with with-er methods. It's what happens when you try to shove immutability
into something that is not actually being used in an immutable manner.
Don't pretend things are immutable when they aren't...
Regards,
Nikita
On Mon, Mar 23, 2020 at 1:48 AM Larry Garfield larry@garfieldtech.com
Thanks for the write-up Larry. I like where you're going with this.
If we were starting from a blank slate design, I would advocate for:
a) having an object initializer syntax
b) not having first-class constructors at all
c) using named constructors instead.This also happens to be exactly what Rust does, go figure... Unfortunately
this kind of approach is hard to retrofit into PHP, because we already have
constructors, almost all classes define them, and it's hard to reconcile
object initialization syntax and non-trivial constructors in a meaningful
way. Combining this with the "no public properties" cargo cult we have
inherited from Java, the paradigm shift is probably too large here.
If wishes were horses, I'd agree. Though both you and Mate mentioned named constructors; I am not sure how that would play into the compacted constructor here.
If we can't have object initializers, then improving what we can do with
constructors is the next best thing :)I generally like the ideal of combining property declaration and
constructors. I've had this on my mind for a while already, and also
received the same suggestion from a couple of other people (I think Nicolas
was one of them?) The current amount of boilerplate that is needed is just
large enough that I will often go with a quick and simple ad-hoc array
structure rather than declaring an explicit value object type. The main
concern, as others have already mentioned, is that these inline
declarations can end up being quite verbose, especially once attributes get
involved.I think I will write up a quick implementation & RFC for this part, as it
seems like something we should at least consider in more detail.
I look forward to it! I am quite open to alternate syntaxes that are more amenable to attributes and delegation, as long as the net result is what we're after: Less repetition so making record-like classes is easier.
Named parameters are a pretty tough topic. I think one of the main points
of contention is that they make the parameters names part of the API
contract, and as such also subject to LSP. Your proposal offers two
possible ways to side-step this: First, by making named parameters opt-in
with a special syntax {}. Second, by limiting them to constructors. The
latter variant still exposes parameter names in the API, but at least does
not require their preservation across inheritance, as constructors are
excluded from LSP. I'm somewhat torn on this, because it makes named
parameters unusable with the very large body of existing methods, and
introduces an inconsistency in which methods can use named params and which
don't.
My own feeling here is that making people care about parameter names is not actually that big of a deal. You should really be caring about variable names anyway. Python seems to do fine with named parameters being part of the contract AFAIK. Especially if we could get it in for PHP 8, that's a major anyway, so I would be fine with it.
The other options (opt-in or constructor only) are IMO fallbacks in case we're nervous about it, or if the parser ends up being happier with a more explicit syntax. (Gotta keep the parser happy.)
Regarding the remainder, I think that all of readonly properties,
asymmetric visibility and property accessors have their place and value,
with some overlap between them. As you already mentioned, the previous
property accessors proposal also included asymettric visibility as a
special case, and that's how I would introduce it as well.However, I generally think that the main value really is the readonly
properties as proposed in the recent RFC. Nowadays, a large fraction of the
classes I use are immutable value objects, for which public readonly
properties provide a much closer match to the semantics I want.I think that the problem with with-er methods is just that: It's a problem
with with-er methods. It's what happens when you try to shove immutability
into something that is not actually being used in an immutable manner.
Don't pretend things are immutable when they aren't...Regards,
Nikita
I agree that Withers are solving an odd problem; however, it's the same approach that PHP itself takes already. Consider DateTimeImmutable:
$d = new DateTimeImmutable();
$d2 = $d->setDate(2020, 1, 3)
->setTime(12, 45)
->setTimezone(new DateTimeZone('America/Chicago')
->modify('+1 week');
That's a Wither pattern. The names a a bit wonky for compatibility with DateTime, but that's the exact approach that Wither methods model. From a user perspective it's pretty good.
If the answer to that is "well don't do that", then what's the alternative? PHP offers no other syntax for evolvable immutable objects than private properties with Wither methods. Making Wither methods harder makes evolvable immutable objects harder. Unless there's some entirely different approach I am not aware of to achieve the same goal, in which case please share. :-)
Asymmetric visibility gives us the same public result as a readonly flag but doesn't break the existing Wither pattern; it means properties are still modifiable internally, but it's way easier to have self-discipline and not muck with a private property by convention than for a public property. It also flows naturally into property accessors, whereas I really don't know how a readonly flag would interact with them.
--Larry Garfield
If the answer to that is "well don't do that", then what's the
alternative? PHP offers no other syntax for evolvable immutable objects
than private properties with Wither methods. Making Wither methods harder
makes evolvable immutable objects harder. Unless there's some entirely
different approach I am not aware of to achieve the same goal, in which
case please share. :-)
For transparency purposes: the idea I presented during the discussion of
"write-once" properties was
to make the following syntax (or a similar one) possible in order to make
cloning of these properties possible:
$self = clone $this with {property1: "foo", ...};
It would clone the object and in the same time change the listed properties
(no matter if they have the "write-once"
flag). It would also take visibility rules into account, so a private
property could only be modified in the private scope.
I think this idea would address your concerns, although I haven't received
any feedback about it yet,
so I'm not sure if it has any gotchas/edge cases that would make it or its
implementation infeasible.
Máté
https://hive.blog/php/@crell/improving-php-s-object-ergonomics
Thanks Larry, that's a good way to move forward on these topics.
I generally like the ideal of combining property declaration and
constructors. I've had this on my mind for a while already, and also
received the same suggestion from a couple of other people (I think Nicolas
was one of them?)
I confirm: that'd be super useful to clean the boilerplate.
large enough that I will often go with a quick and simple ad-hoc array
structure rather than declaring an explicit value object type.
Yes
The main concern, as others have already mentioned, is that these inline
declarations can end up being quite verbose, especially once attributes get
involved.
I don't share this concern with attributes: they mix without any ambiguity,
which is what matters.
No syntax is perfect, ppl that prefer the current way will still be able to
use it.
Named parameters are a pretty tough topic. I think one of the main points
of contention is that they make the parameters names part of the API
contract, and as such also subject to LSP. Your proposal offers two
possible ways to side-step this: First, by making named parameters opt-in
with a special syntax {}. Second, by limiting them to constructors. The
latter variant still exposes parameter names in the API, but at least does
not require their preservation across inheritance, as constructors are
excluded from LSP. I'm somewhat torn on this, because it makes named
parameters unusable with the very large body of existing methods, and
introduces an inconsistency in which methods can use named params and which
don't.
I'd like to propose something on the topic.
I'm adding object literals to the mix because that's another feature of the
language that we're missing a lot IMHO.
Actually, there is one existing syntax for objects: (object) [...]
My proposal is to allow any class in the casting operator: (Foo) [...]
By default, all keys of the casted array would map to properties (with an
error when no matching property exists in the current visibility scope). We
would then allow for a new constructor method, either:
public function __create(array $values)
or:
public static function __create(array $values): static
This method would take over the cast operator and decide how to construct
an instance of such a class.
There is one drawback: accepted keys are not documented. Sure, property
declarations can give a big hint.
But I think we can solve this issue later: it's not a blocker to still make
things work nicely. Also, this issue already exists with all methods that
accept an array of options - and we'll find a solution for those - either
using docblocks (there are already ways to use them for that) or using
attributes (would be the best of course, once we have them.)
Note that this __create() method looks a lot like __unserialize(): it's
just called at a different place, but the implementations could be
essentially the same.
Regarding the remainder, I think that all of readonly properties,
asymmetric visibility and property accessors have their place and value,
with some overlap between them. As you already mentioned, the previous
property accessors proposal also included asymettric visibility as a
special case, and that's how I would introduce it as well.
Máté suggested this syntax and it has my preference over the one you menton
Larry: doubling the visibility keyword could be enough to express
read+write access:
public private $property; <= public read access, private write access
However, I generally think that the main value really is the readonly
properties as proposed in the recent RFC. Nowadays, a large fraction of the
classes I use are immutable value objects, for which public readonly
properties provide a much closer match to the semantics I want.I think that the problem with with-er methods is just that: It's a problem
with with-er methods. It's what happens when you try to shove immutability
into something that is not actually being used in an immutable manner.
Don't pretend things are immutable when they aren't...
I think "withers" solve the problem of immutability in a very pragmatic and
convenient way.
Take e.g. "private": it can be bypassed using closure rebinding or
reflection, and this is really useful, e.g. to build "friendship" relations
between classes in the same package (like in C++).
On the other side, "final" is a real pain when needing to write
proxies/decorators. It just blocks extensibility for no technical reasons
really. Let me explain: If as a consumer, I DO want to extend a class,
nothing will prevent me to do so. There is always the last resort solution,
which is patching the too restrictive source class, either using a fork or
some code rewriting tool.
Either way, what matters is who is in charge of dealing with the issues
this might create. In both ways, what matters is that the original author
won't be bothered for things that are not its responsibility. "I broke your
app because you messed up with that private property on a class I
authored?" no my problem. About final, I usually prefer using the "@final"
annotation: it expressed exactly what I need to express as the author of
the code: "if you extend, you're on your own - but I'm not dictating what
you can/can't do either".
I think this reasoning applies to my view on immutability :)
Cheers,
Nicolas
https://hive.blog/php/@crell/improving-php-s-object-ergonomics
Named parameters are a pretty tough topic. I think one of the main points
of contention is that they make the parameters names part of the API
contract, and as such also subject to LSP. Your proposal offers two
possible ways to side-step this: First, by making named parameters opt-in
with a special syntax {}. Second, by limiting them to constructors. The
latter variant still exposes parameter names in the API, but at least does
not require their preservation across inheritance, as constructors are
excluded from LSP. I'm somewhat torn on this, because it makes named
parameters unusable with the very large body of existing methods, and
introduces an inconsistency in which methods can use named params and which
don't.I'd like to propose something on the topic.
I'm adding object literals to the mix because that's another feature of the
language that we're missing a lot IMHO.
Actually, there is one existing syntax for objects: (object) [...]My proposal is to allow any class in the casting operator: (Foo) [...]
By default, all keys of the casted array would map to properties (with an
error when no matching property exists in the current visibility scope). We
would then allow for a new constructor method, either:
public function __create(array $values)
or:
public static function __create(array $values): staticThis method would take over the cast operator and decide how to construct
an instance of such a class.There is one drawback: accepted keys are not documented. Sure, property
declarations can give a big hint.But I think we can solve this issue later: it's not a blocker to still make
things work nicely. Also, this issue already exists with all methods that
accept an array of options - and we'll find a solution for those - either
using docblocks (there are already ways to use them for that) or using
attributes (would be the best of course, once we have them.)Note that this __create() method looks a lot like __unserialize(): it's
just called at a different place, but the implementations could be
essentially the same.
Interesting. If that allowed populating private properties than that would be more capable than either of the initializer proposals that have been put forward.
However, this alternate deserialization (as you note, it's basically __unserialize by another name) brings up other issues:
-
If it's a large number of properties, the __create method would still need to manually assign them to properties, just like constructors now. It wouldn't benefit from constructor promotion.
-
What's its order of execution with the constructor? Vis, does the constructor run after __create or before? Does __construct get any parameters passed to it? If it doesn't get called, that's yet another way to bypass the constructor and therefore bypass object validation. ("Make invalid states unrepresentable". If the properties don't logically make sense with each other that should be rejected as early as possible; syntactically if we can, via early validation if not.)
Named parameters have neither of those issues, as data still only comes in via the constructor so both promotion and validation work fine.
Regarding the remainder, I think that all of readonly properties,
asymmetric visibility and property accessors have their place and value,
with some overlap between them. As you already mentioned, the previous
property accessors proposal also included asymettric visibility as a
special case, and that's how I would introduce it as well.Máté suggested this syntax and it has my preference over the one you menton
Larry: doubling the visibility keyword could be enough to express
read+write access:public private $property; <= public read access, private write access
In that syntax, you have to remember which one comes first. There's no indication for the casual reader why
public private $property;
and
private public $property;
are different. Plus, just looking at it, "wait, it's public and private? WTF? That doesn't even make sense."
It also doesn't extend gracefully to property accessors. Whatever accessors do, if they ever get resolved, would conflict with that, and thus we'd have that many more weird combinations of property metadata that are incompatible.
The whole point of the syntax I proposed for asymmetric visibility is that it's gracefully extensible, even if a little more verbose. (If we can find a syntax that is less verbose while still gracefully extensible, I am on board with that.)
--Larry Garfield
Am 25.03.20 um 15:24 schrieb Larry Garfield:
Máté suggested this syntax and it has my preference over the one you menton
Larry: doubling the visibility keyword could be enough to express
read+write access:public private $property; <= public read access, private write access
In that syntax, you have to remember which one comes first. There's no indication for the casual reader why
public private $property;
and
private public $property;
are different. Plus, just looking at it, "wait, it's public and private? WTF? That doesn't even make sense."
It also doesn't extend gracefully to property accessors. Whatever accessors do, if they ever get resolved, would conflict with that, and thus we'd have that many more weird combinations of property metadata that are incompatible.
The whole point of the syntax I proposed for asymmetric visibility is that it's gracefully extensible, even if a little more verbose. (If we can find a syntax that is less verbose while still gracefully extensible, I am on board with that.)
What about the following syntax:
class X {
public read private write $property;
}
That would play nicely with accessors:
class Y {
public read getProp private write setProp Type $property;
// or this way around to make it clear which is the setter
// and which is the property type
public getProp read private setProp write Type $property;
private function getProp() : ?Type {}
private function setProp (Type $newValue) {}
}
It seems to me this would allow a clear syntax where you can selectively
add accessor method for reading or writing or both, you can reuse
existing setting methods when refactoring.
Only the isset/unset accessors are missing. I am not sure if they are
necessary: isset could be equivalent to ($obj->getProp() !== null) and
unset to setting null, but I have not thought this through yet.
Greets
Dennis
Hi Larry and Nicolas,
I'd like to propose something on the topic.
I'm adding object literals to the mix because that's another feature of the
language that we're missing a lot IMHO.
Actually, there is one existing syntax for objects: (object) [...][...]
- What's its order of execution with the constructor? Vis, does the constructor run after __create or before? Does __construct get any parameters passed to it?
That's the interesting question, though there's probably not any
sensible answer that works for all use cases. However, if this kind of
object casting (object literal notation) was limited to a certain
(extendable) internal class (SPL Struct?), we could have just a final
public function __construct(array $properties = []) that then
delegates the validation to an abstract __validation() method. Would
that at least be a useful alternative to constructor promotion?
In that syntax, you have to remember which one comes first. There's no indication for the casual reader why
public private $property;
and
private public $property;
I'm wondering - are there any use cases where you would have private
read and public write?
If not, maybe this issue can be simplified by adding (f.ex.)
"semiprivate" and "semiprotected" attribute keywords, the idea being
that they give readonly access to the anything else.
Though I think it would be great to have a nice, gracefully extensible
attribute syntax, maybe it's not necessary to fight over that for this
particular issue?
Either way, what matters is who is in charge of dealing with the issues
this might create. In both ways, what matters is that the original author
won't be bothered for things that are not its responsibility. "I broke your
app because you messed up with that private property on a class I
authored?" no my problem. About final, I usually prefer using the "@final"
annotation: it expressed exactly what I need to express as the author of
the code: "if you extend, you're on your own - but I'm not dictating what
you can/can't do either".I think this reasoning applies to my view on immutability :)
:-)
I can relate to that in general.
Though for immutability, I'm not so sure anymore...
For details, see the full writeup:
https://hive.blog/php/@crell/improving-php-s-object-ergonomics
An excellent writeup, thank you Larry.
Peter