Re: [PHP-DEV] [RFC] Object Initializer

This is only part of a thread. view whole thread
  107074
September 13, 2019 10:18 rasmus@mindplay.dk (Rasmus Schultz)
I'd like to address the examples - and why I think they don't demonstrate
that this feature is really useful in practice.

There are several examples similar to this one:

class Car
{
  public int $yearOfProduction;
  public string $vin;
}

This is either lacking a constructor to initialize the properties to the
defined types - or it's lacking nullable type-hints. As it is, this class
doesn't guarantee initialization according to it's own property type
constraints.

Assuming the fields of this entity are required, you would probably prefer
to add a constructor - but then property initializers aren't really useful
anymore. This seems to be the case for most of the examples.

Models often have other constraints besides just the type - in those cases,
your models would likely have private/protected fields and setter-methods
that implement those constraints. The visibility example demonstrates the
use of a static factory method:

class Customer
{
  private string $name = '';
  protected ?string $email = null;

  public static function create(string $name, ?string $email = null): self
  {
    return new self {
      name = $name, // assign private property within the same class
      email = $email, // assign protected property within the same class
    };
  }
}

This may be fine for some use-cases - but many model types are only going
to have one valid way to construct an instance, and constructors are the
idiomatic way to do that. Unfortunately, this language feature works for
the new-statement only, so a desire to use this language feature will drive
architecture.

All in all, I find this feature is useful or applicable only to a few,
select patterns within the language - it isn't general enough.

In my opinion, language features should be as general as possible - a
feature like this "looks nice", being very abbreviated and clean-looking,
and, as I believe the examples in the RFC itself demonstrates, this will
provide the wrong kind of motivation to make what are, effectively,
architectural decisions.

Some models are immutable by design. Those cases don't seem to be well
supported by this feature.

My strong preference over this feature would be named parameters, which can
provide the same abbreviated initializations, but works more consistently
with the language, e.g. for all of the use-cases I cited above.

It works for constructors:

class Car
{
  public int $yearOfProduction;
  public string $vin;

  public function __construct(int $yearOfProduction, string $vin) {
    if ($yearOfProduction < 1900 ||  $yearOfProduction > date("Y")) {
      throw new InvalidArgumentException("year of production out of range:
{$yearOfProduction}");
    }

    $this->yearOfProduction = $yearOfProduction;
    $this->vin = $vin;
  }
}

$car = new Car({ yearOfProduction = 1975, vin = "12345678"});

It works for static factory-methods:

$car = Car::create({ yearOfProduction = 1975, vin = "12345678"});

It works for models with private/protected fields, classes with accessors,
classes with validations in constructors or factory-methods, and so on.

In other words, it works more generally with all common patterns and
practices - in many ways, it just seems like a better fit for the language.

The common criticism against named parameters, is they create coupling to
parameter-names. Object initializers create coupling to property-names - if
you can live with that, I don't think named parameters or object
initializers are very different in that regard.

The only problem I see with named parameters as an alternative to object
initializers, is in terms of versioning - renaming an argument, today, is
not a breaking change, and now it would be. That could be addressed, for
example, by making named parameters explicit at the declaration site - for
example, use curly braces in the declaration to designate named arguments:

  function makeCar({ int $yearOfProduction, string $vin }) {
    // ...
  }

This way, adding named parameters is not a breaking change - it's an opt-in
feature for those cases where it's useful and meaningful, but it's still
applicable to all the patterns and cases that someone might want to use it
for.

Named parameters is just one possible alternative - I'm just naming it for
practical comparison, to demonstrate how some features may have more
general applications than others.

I'd prefer to see new features that work everywhere, all the time, for
everyone - and for existing code. Rather than adding more features and
syntax for very specific (even relatively rare) use-cases.


On Thu, Sep 12, 2019 at 4:00 PM Michał Brzuchalski <
michal.brzuchalski@gmail.com> wrote:

> Hi internals, > > I'd like to open discussion about RFC: Object Initializer. > > This proposal reduces boilerplate of object instantiation and properties > initialization in case of classes without required constructor arguments as > a single expression with initializer block. > > https://wiki.php.net/rfc/object-initializer > > I appreciate any feedback you all can provide. > > Thanks, > -- > Michał Brzuchalski > brzuchal@php.net >
  107077
September 13, 2019 12:30 arnold.adaniels.nl@gmail.com (Arnold Daniels)
On Fri, Sep 13, 2019 at 12:18 PM Rasmus Schultz <rasmus@mindplay.dk> wrote:

> I'd like to address the examples - and why I think they don't demonstrate > that this feature is really useful in practice. > > There are several examples similar to this one: > > class Car > { > public int $yearOfProduction; > public string $vin; > } > > This is either lacking a constructor to initialize the properties to the > defined types - or it's lacking nullable type-hints. As it is, this class > doesn't guarantee initialization according to it's own property type > constraints. > > Assuming the fields of this entity are required, you would probably prefer > to add a constructor - but then property initializers aren't really useful > anymore. This seems to be the case for most of the examples. >
Using initializers and a constructor and not mutually exclusive. class User { public string $id; public ?string $name; public ?string $email; public function __construct() { $this->id = bin2hex(random_bytes(16)); } }
> > Models often have other constraints besides just the type - in those cases, > your models would likely have private/protected fields and setter-methods > that implement those constraints. The visibility example demonstrates the > use of a static factory method: > > class Customer > { > private string $name = ''; > protected ?string $email = null; > > public static function create(string $name, ?string $email = null): self > { > return new self { > name = $name, // assign private property within the same class > email = $email, // assign protected property within the same class > }; > } > } > > This may be fine for some use-cases - but many model types are only going > to have one valid way to construct an instance, and constructors are the > idiomatic way to do that. Unfortunately, this language feature works for > the new-statement only, so a desire to use this language feature will drive > architecture. >
This example doesn't make a lot of sense. In this case, the `create` method has no added benefit. You can just as well just use the constructor. class Customer { private string $name; protected ?string $email; public function __construct(string $name = '', ?string $email = null) { $this->name = $name; $this->email = $email; } } Using constructor arguments isn't a great approach classes that have a large number of properties, which is typically the case with data objects.
> All in all, I find this feature is useful or applicable only to a few, > select patterns within the language - it isn't general enough.
> In my opinion, language features should be as general as possible - a > feature like this "looks nice", being very abbreviated and clean-looking, > and, as I believe the examples in the RFC itself demonstrates, this will > provide the wrong kind of motivation to make what are, effectively, > architectural decisions. >
It seems like you consider the use of public properties as bad practice in general. However, I do not think such a hard stance should be taken in the ongoing discussion about public properties vs getters and setters. There are valid arguments on both sides. If you don't use public properties, this RFC will not affect you at all. If you do, there is a clear benefit in this approach.
> > Some models are immutable by design. Those cases don't seem to be well > supported by this feature. >
Immutable objects are not well supported in general in PHP. This RFC doesn't affect it.
> My strong preference over this feature would be named parameters, which can > provide the same abbreviated initializations, but works more consistently > with the language, e.g. for all of the use-cases I cited above. > > It works for constructors: > > class Car > { > public int $yearOfProduction; > public string $vin; > > public function __construct(int $yearOfProduction, string $vin) { > if ($yearOfProduction < 1900 || $yearOfProduction > date("Y")) { > throw new InvalidArgumentException("year of production out of range: > {$yearOfProduction}"); > } > > $this->yearOfProduction = $yearOfProduction; > $this->vin = $vin; > } > } > > $car = new Car({ yearOfProduction = 1975, vin = "12345678"}); > > It works for static factory-methods: > > $car = Car::create({ yearOfProduction = 1975, vin = "12345678"}); > > It works for models with private/protected fields, classes with accessors, > classes with validations in constructors or factory-methods, and so on. > > In other words, it works more generally with all common patterns and > practices - in many ways, it just seems like a better fit for the language. >
I see how named parameters competes with this RFC. They are two different things, that may both be implemented. The need to define all properties as constructor arguments and then setting them all isn't a great approach for classes with 10+ properties as it really bloats the class. Also having property guard for public properties that are only in the constructor isn't that great. The common criticism against named parameters, is they create coupling to
> parameter-names. Object initializers create coupling to property-names - if > you can live with that, I don't think named parameters or object > initializers are very different in that regard. >
You always have coupling to public property names. Public properties and public methods are what make up the api of the class. Nothing in this RFC changes that.
> > The only problem I see with named parameters as an alternative to object > initializers, is in terms of versioning - renaming an argument, today, is > not a breaking change, and now it would be. That could be addressed, for > example, by making named parameters explicit at the declaration site - for > example, use curly braces in the declaration to designate named arguments: > > function makeCar({ int $yearOfProduction, string $vin }) { > // ... > } > > This way, adding named parameters is not a breaking change - it's an opt-in > feature for those cases where it's useful and meaningful, but it's still > applicable to all the patterns and cases that someone might want to use it > for. >
This is a discussion on itself. It has nothing to do with this RFC. Please create a new RFC to discuss it.
> > Named parameters is just one possible alternative - I'm just naming it for > practical comparison, to demonstrate how some features may have more > general applications than others.
> I'd prefer to see new features that work everywhere, all the time, for > everyone - and for existing code. Rather than adding more features and > syntax for very specific (even relatively rare) use-cases. >
There is really nothing rare about the use of public properties. The RFC shouldn't be invalidated because it doesn't benefit one particular programming style, especially if that style is under heavy debate.
  107080
September 13, 2019 14:35 rowan.collins@gmail.com (Rowan Tommins)
Hi Rasmus,

On Fri, 13 Sep 2019 at 11:18, Rasmus Schultz <rasmus@mindplay.dk> wrote:

> All in all, I find this feature is useful or applicable only to a few, > select patterns within the language - it isn't general enough. >
I've trimmed the quote for readability, but agree with basically everything in this message. :) I like the reasoning behind this RFC, but think it is unnecessarily limited. It's not about "public properties are bad", it's just that if we can come up with a feature that works for public properties *and* other programming styles, we should prefer that. A related proposal that's come up before is short-hand constructors: class Foo { private int $foo; public function __construct($this->foo) { // automatically assigns first parameter to $this->foo so body is simplified } } Combine that with opt-in named parameters, and the Customer example from the RFC might look something like this: class Customer { private $id; private $name; private DateTimeImmutable $createdAt; public function __construct(id => $this->id, name => $this->name, createdDate => string $dateString = 'now') { $this->createdAt = new DateTimeImmutable($dateString); } }$customer = new Customer( id => 123, name => 'John Doe', createdDate => '2019-01-01 12:34' ); It's slightly more verbose, but a lot more flexible. As a side note, I have always thought stdClass was a bit of a kludge, and now we have real anonymous classes I would love to see it gradually phased out. I would much rather see syntax for capturing variables in an anonymous class declaration than new ways to create stdClass objects. Regards, -- Rowan Tommins [IMSoP]
  107093
September 14, 2019 20:29 larry@garfieldtech.com ("Larry Garfield")
On Fri, Sep 13, 2019, at 9:35 AM, Rowan Tommins wrote:
> Hi Rasmus, > > On Fri, 13 Sep 2019 at 11:18, Rasmus Schultz <rasmus@mindplay.dk> wrote: > > > All in all, I find this feature is useful or applicable only to a few, > > select patterns within the language - it isn't general enough. > > > > > I've trimmed the quote for readability, but agree with basically everything > in this message. :) > > I like the reasoning behind this RFC, but think it is unnecessarily > limited. It's not about "public properties are bad", it's just that if we > can come up with a feature that works for public properties *and* other > programming styles, we should prefer that. > > A related proposal that's come up before is short-hand constructors: > > class Foo { > private int $foo; > public function __construct($this->foo) { > // automatically assigns first parameter to $this->foo so body is > simplified > } > } > > Combine that with opt-in named parameters, and the Customer example from > the RFC might look something like this: > > class Customer > { > private $id; > private $name; > private DateTimeImmutable $createdAt; > > public function __construct(id => $this->id, name => $this->name, > createdDate => string $dateString = 'now') > { > $this->createdAt = new DateTimeImmutable($dateString); > } > }$customer = new Customer( > id => 123, > name => 'John Doe', > createdDate => '2019-01-01 12:34' > ); > > > It's slightly more verbose, but a lot more flexible. > > > As a side note, I have always thought stdClass was a bit of a kludge, and > now we have real anonymous classes I would love to see it gradually phased > out. I would much rather see syntax for capturing variables in an anonymous > class declaration than new ways to create stdClass objects. > > Regards, > -- > Rowan Tommins > [IMSoP]
I'm a big fan of using defined classes over anon arrays for struct-like data, for various reasons (cf https://steemit.com/php/@crell/php-use-associative-arrays-basically-never), so I'm sympathetic toward anything that makes that easier. However, I have to agree with Rasmus and Rowan that the current RFC proposal is too "narrow" to solve that effectively. The problem to be solved is that this: class Employee { public int $age; public string $name; public ?Employee $boss; public function __construct(int $age, string $name, Employee $boss = null) { $this->age = $age; $this->name = $name; $this->boss = $boss; } } $e = new Employee(34, "Bob", $sally); // or $e = new Employee(); $e->age = 34; $e->name = 'Bob'; $e->boss = $sally; Is just annoyingly verbose and annoying to work with. I agree. However, initializers as presented solve only a subset of this problem space: The case where: * The properties are public. * The constructor can be viable with no parameters. * There is no needed validation, or we can expect a user to manually call validate() or similar afterward. While that case absolutely exists, it is only a subset of the relevant use cases. As Rasmus and Rowan note, however, breaking the problem apart into two pieces would allow it to handle a wider array of use cases while still solving the one presented. For example (and with no thought given to syntax here, 'hoist' is almost certainly the wrong word): class Employee { protected int $age; protected string $name; protected ?Employee $boss; public function __construct({hoist int $age, hoist string $name, hoist Employee $boss = null}) { if ($age < 18) throw new ChildLaborException(); } } $e = new Employee({ age = 34, name = 'Bob', boss = $sally, }); Solves the initializer use case with very similar syntax for the caller, BUT also works for: * Any function where named parameters would be useful, independent of object initialization. * Any object where the properties in question are not public. * Any object where validation is required and you don't trust the caller to remember to call it (which you shouldn't trust) * Any object where only some of the constructor parameters map to object properties, a fact that should be hidden from the caller. And probably other situations. I could envision taking it a step further and writing instead: class Employee { public function __construct({hoist private int $age, hoist protected string $name, hoist public ?Employee $boss = null}) { if ($age < 18) throw new ChildLaborException(); } } And now you have even less typing and repetition. If you wanted to go really really far, you could even: class Employee { protected int $age; protected string $name; protected ?Employee $boss; public function hoist __construct() { if ($age < 18) throw new ChildLaborException(); } } As a short hand that enables any property to be passed in, but only using a named call, and still enforce that some parameters are required. I could see that being very useful for service objects, as well as value objects, where there is almost always a 1:1 mappnig from properties to constructor parameters. That is, named arguments are useful on their own (and have been discussed before). Auto-populating properties from the constructor is useful on its own. With the combination of those, the need for a specialized initializer syntax goes away, because they are an emergent property of the other two features. So on net, we get more power and more flexibility for less new syntax. Implementing a more limited syntax (just initialization for a subset of use cases) makes implementing the more robust approach in the future harder, because we'd have to be super careful to avoid BC issues or else end up with two slightly different syntaxes for slightly different but really the same thing behaviors; that's not only bad for usability and learnability but we're running out of convenient sigils. :-) So I would be -1 on initializers as proposed, but +1 on addressing that problem space through the more robust combination of named arguments and auto-populating properties. --Larry Garfield