On Fri, Aug 5, 2022, at 12:08 PM, Larry Garfield wrote:
> Ilija Tovilo and I are happy to present the first new RFC for PHP 8.3: 
> Asymmetric Visibility.
>
> https://wiki.php.net/rfc/asymmetric-visibility
>
> Details are in the RFC, but it's largely a copy of Swift's support for the 
> same.

Hi folks. Ilija and I have been discussing and experimenting extensively based 
on the feedback to the first draft.  Here's our thoughts so far, some 
additional data, and a request for feedback.  (These changes haven't been made 
to the RFC just yet, as there's outstanding questions for which we want 
feedback below.)

1. Rather than `readonly` being forbidden on asymmetric properties, it's better 
to redefine it as "write once, and if no `set` visibility is defined, make it 
private."  That would still allow for `public protected(set) readonly string 
$blah` (to work around existing limitations in `readonly`), but also more or 
less preserve the current syntax in a logical way.

2. Given that... we're not sure that it makes sense to have any visibility 
scope other than `get`/`set`.  `once` becomes unnecessary with that 
understanding of `readonly`, and we cannot think of any other examples that are 
not a stretch.  So that axis of extensibility is of minimal importance.

3. Since there has been some confusion, our goal *is* to implement property 
accessors in the future as a follow-on to this RFC.  However, conceptually 
property "hooks" and asymmetric visibility are separate features.  They also 
both have ample bikeshed potential.  That's why we're proposing them as two 
separate RFCs.  (Technically property hooks could also be done first, but this 
one is smaller.)  That said, it's become obvious that we cannot avoid 
discussing property hooks at the same time since the syntax does overlap.  So 
in the discussion below I will include property hooks, but deliberately gloss 
over parts that are not relevant right now.  Please humor me and do the same. 
:-)

4. In concept, there's two main syntactic models for property hooks/accessors: 
Javascript/Python style (annotated methods) and Swift/C# style (method-esque 
code blocks on properties).  We were trying to keep our options open and avoid 
a syntax that locks us into one or the other.  However, we've concluded that 
the annotated methods style just wouldn't work at all in a language with 
explicit types and visibility controls.  We've therefore made the executive 
decision that only the Swift/C# style is an option.

5. That leaves the basic question of "where to put the visibility," as Swift 
and C# put them in different places.  (There are other differences not relevant 
for now.)

Swift:

```swift
class User {
   public private(set) var first: String
   public private(set) var last: String
  
   public private(set) fullName: String {
       get { ... }
       set { ... }
   }
}
```

C#:

```csharp
class User {
   public string first { get; private set; }
   public string last { get; private set; }
  
   public string fullName {
       get { ... }
       private set { ... }
   }
}
```

The relevant difference is that in Swift, all visibility is on the left of the 
property while in C# it is split between the left and right.

The main argument for Swift-style: Keep everything in one place, and if there's 
no property hooks we don't even have to think about it.

The main argument for C#-style: It avoids repeating a keyword (eg, `set`) if 
using both features.

It *may* be possible to omit the `get` in the C# example above if it's just the 
default. Ilija is still working to figure that out.

## References make everything worse

One other caveat to consider is (naturally) references. A `get` hook cannot 
return by reference, as that would allow setting the value via its reference 
and bypassing a `set` hook.

A side effect of that restriction is that arrays cannot be modified in place on 
properties with a `get` hook.  That is, assuming the `$bar` property is an 
array that leverages the extra hooks above (by whatever syntax), the following 
is not possible:

```php
$f = new Foo();

$f->bar[] = 'beep';
```

That would require returning `$bar` by reference so that the `[]` operation 
could apply to it directly.  However, that then bypasses any restrictions 
imposed by a `set` hook (such as the values had to be integers).  The 
following, however, would be legal:

```php
$f = new Foo();

$b = $f->bar;

$b[] = 'beep';

$f->bar = $b;
```

That problem wouldn't happen if all we're changing is visibility, however.  
That means in the C# style, we would need to differentiate whether the block on 
the right is intended to disable references or not.  Ilija has proposed `raw` 
to indicate that.  That would mean:

```php
class Test
{
   public $a { private set; }
   public $b { private raw; }

   public function inScope()
   {
       echo $this->a; // echo $this->getA();
       $this->a = 'a'; // $this->setA('a');
      
       echo $this->b; // echo $this->b;
       $this->b = 'b'; // $this->b = 'b';
   }
}

$test = new Test();

echo $test->a; // echo $test->getA();
$test->a = 'a'; // Error, set operation not accessible

echo $test->b; // echo $test->getB();
$test->b = 'b'; // Error, set operation not accessible
```

The take-away for the asymmetric visibility only case is that we would need to 
use `raw` instead of `set`, in order to avoid confusion later with accessor 
hooks and whether or not to disable references.

The Swift-style syntax, as it does not require any hook-like syntax to just 
control visibility, does not have this problem.  There is no ambiguity to 
resolve, so no need for a `raw` keyword.

## Common use cases

While there are a myriad of possible ways to use both asymmetric visibility and 
property accessor hooks, we anticipate three usage patterns to predominate:

1. Public read, private/protected write, but that's it.
2. Public read, public write, but some extra validation on set.
3. Public lazily-derived value, no write.

In particular, it seems to us (though we have no precise data to back it up) 
that it is unlikely that both asymmetric visibility and property hooks would be 
used at the same time, in the typical cases.  That should inform the discussion 
about syntax.

## Comparison

For the moment I'm just going to use Swift-style as-is.  There's a discussion 
later to be had about minor variations on it, as suggested by a few people in 
the previous thread.  Stand by.

Length comparison:

```php
// Current code, for comparison
public readonly string $first;

// Swift-ish
public private(set) string $first;

// C#-ish, note "raw" because there's no hook
public string $first { get; private raw; }
```

Both options suggest possible shorthands:

```php
// Current code, for comparison
public readonly string $first;

// Swift-ish
private(set) string $first;

// C#-ish, assuming it can be done
public string $first { private raw; }
```

If combining with `readonly`, we get:

```php
// Current code, for comparison
public readonly string $first;

// Swift-ish
public protected(set) readonly string $first;
protected(set) readonly string $first;

// C#-ish
public readonly string $first { get; private set; }
public readonly string $first { private raw; }
```

If we mix in for-reals accessor hooks, we get:

```php
// Swift-ish
public private(set) string $first {
   get { ... }
   set { ... }
}

// C#-ish, note now we're using "set".
public string $first {
   get { ... }
   private set { ... }
}
```

If other hooks are added (Swift has `beforeSet` and `didSet`, and we intend to 
include those as well for PHP), they don't really make sense to have visibility 
modifiers on them.  It's arguable if `isset` or `unset` (to mirror the existing 
magic methods) would want those.  Does it make sense to have "public read, 
public set, but private unset"?  Or is `unset` just controlled by `set`?  
Unclear.

Another complication is that, for forward compatibility, we would need a way to 
disable references on no-hook properties so that hooks could be added later 
without a change in reference behavior.  Given the postfix syntax for hooks, we 
believe the most natural way to do that would be:

```php
// Swift-ish
public string $foo {}

// C#-ish
public string $foo {}
// which is shorthand for:
public string $foo { public raw; }
```

Again, the C# model implies a bit more complexity and nuance.


## Questions

Question 1: Which general model (Swift or C#) is preferable, and why?

(At the moment, Ilija and I disagree on which one is preferable.  Hence, poll.)

## If Swift

If we go with Swift-style, several people suggested alternative syntax options. 
 This is *mostly* a bikeshed question, so I will just list them:

```php
public private(set) string $first;
public private:set string $first;
public:private string $first;
public private string $first;
public(set: private) string $first;
```

Some of which have natural shorthands:

```php
private(set) string $first;
private:set string $first;
:private string $first;
N/A
N/A
```

Question 2: IF we go with Swift-style, which of the above syntaxes would you 
prefer, and why?


Please answer both questions only via this poll so can collect actual data.

https://docs.google.com/forms/d/e/1FAIpQLSefq15VvGNIXSnQaMTl3RW451w0E8oesny8c4PLqmKl8HhQ-Q/viewform

Thank you all for your time and input.

--Larry Garfield and Ilija Tovilo

-- 
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: https://www.php.net/unsub.php

Reply via email to