# [RFC][Draft] Sealed Classes

- Thread: https://externals.io/message/114116
- Original archive: https://news-web.php.net/php.internals/114116
- Started by Saif Eddin Gmati on 2021-04-24T10:55:27+00:00
- Messages: 73
- RFC: Sealed Class (Declined) — https://externals.io/rfc/sealed_classes.md
- Page 1 of 3: messages 1 to 25 (25 per page, add `?page=N` to the URL)
- Next page: https://externals.io/message/114116.md?page=2
- Reply by email to internals@lists.php.net
- More for agents: search with https://externals.io/search.md?q={query} · RFCs at https://externals.io/rfc.md · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

---

## #114116 — Saif Eddin Gmati — 2021-04-24T10:55:27+00:00

URL: https://externals.io/message/114116#114116

Hello Internals,

I'm sending this email to open discussion about sealed classes, interfaces, and traits feature for PHP 8.1.

I have create a Draft RFC here: https://wiki.php.net/rfc/sealed_classes

A major concern for few people have been the syntax, in which it introduces 2 new keywords into the languages, therefor, i have added a section about alternative syntax which could be used to avoid this problem.

Regards,

Saif.

---

## #114117 — Saif Eddin Gmati — 2021-04-24T11:00:38+00:00

URL: https://externals.io/message/114116#114117 · In reply to: #114116

A major behavior i wanted to discuss is how should sealed interfaces work, and specially around `Throwable` which is currently sealed to only `Error` and `Exception`, but allows other interfaces to extend it, and other classes to implement it as long as they extend `Error` or `Exception` ( or another class that does so ).

As per the current proposal, if `Throwable` is to be declared `sealed`, permitting only `Error` and `Exception`, it won't be possible to extend it, resulting in a major BC-break, considering that too many libraries extend this interface in their own `ExceptionInterface`.

To work around this, there's two solution:

1. don't declare `Throwable` sealed and keep it as a special case.
2. interfaces declared sealed, can be extended by other interfaces, however, classes that implement either the sealed interface directly, or via another interface, have to extend one of the classes the sealed interface permits.

The second solution will allow declaring `Throwable` sealed, and would bring complete consistency between internally sealed symbols, and user land, without any BC breaks, and IMHO, it doesn't hurt as at the end, no one will be able to implement `Throwable` without extending the permitted classes.

Note: with the second solution, if an interface is sealed and permits only 2 classes which are final, and you extend that interface into another, there's no way you can actually implement your new interface, and i will be as if you just declared a final interface.

‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐


> Hello Internals,
> 

> I'm sending this email to open discussion about sealed classes, interfaces, and traits feature for PHP 8.1.
> 

> I have create a Draft RFC here: https://wiki.php.net/rfc/sealed_classes
> 

> A major concern for few people have been the syntax, in which it introduces 2 new keywords into the languages, therefor, i have added a section about alternative syntax which could be used to avoid this problem.
> 

> Regards,
> 

> Saif.

---

## #114119 — Pierre — 2021-04-24T12:56:07+00:00

URL: https://externals.io/message/114116#114119 · In reply to: #114116

Le 24/04/2021 à 12:55, Saif Eddin Gmati a écrit :
> Hello Internals,
>
> I'm sending this email to open discussion about sealed classes, 
> interfaces, and traits feature for PHP 8.1.
>
> I have create a Draft RFC here: 
> https://wiki.php.net/rfc/sealed_classes 
> <https://wiki.php.net/rfc/sealed_classes>
>
> A major concern for few people have been the syntax, in which it 
> introduces 2 new keywords into the languages, therefor, i have added a 
> section about alternative syntax which could be used to avoid this 
> problem.
>
> Regards,
>
> Saif.

Hello,

And why not using an attribute, such as in HackLang ?

Regards,

--

Pierre

---

## #114121 — Benjamin Eberlei — 2021-04-24T14:03:32+00:00

URL: https://externals.io/message/114116#114121 · In reply to: #114119

> Le 24/04/2021 à 12:55, Saif Eddin Gmati a écrit :
> > […]
>
> Hello,
>
> And why not using an attribute, such as in HackLang ?
>

+1 on this, I said the same on the "never/noreturn" RFC. There is a much
less invasive way to add new keywords/flags to functions by using
attributes.

Imho this decouples new features from the language and reduces the "risk"
of adding them to the language. That should increase the likeliness of it
getting accepted in my opinion.

>
> Regards,
>
> --
>
> Pierre
>
> --
> 
> To unsubscribe, visit: https://www.php.net/unsub.php

---

## #114123 — Levi Morrison via internals — 2021-04-24T14:43:12+00:00

URL: https://externals.io/message/114116#114123 · In reply to: #114121

>
> 
>
> > […]
>
> +1 on this, I said the same on the "never/noreturn" RFC. There is a much
> less invasive way to add new keywords/flags to functions by using
> attributes.
>
> Imho this decouples new features from the language and reduces the "risk"
> of adding them to the language. That should increase the likeliness of it
> getting accepted in my opinion.

I think an attribute may be appropriate here because sealed types act
like normal types, except we restrict who can extend them.
Additionally, we have to provide data about which types can extend the
sealed type, so it's not just a simple on/off type behavioral switch
(which I think is an antipattern for attributes based on my experience
in other languages that have them).

This is different from a return type `never`. A function which never
returns cannot meaningfully have any return type at all -- using
`void` or some other type with an attribute would be a lie.
Additionally, there isn't any meta-data to associate with the `never`.
I hope this comment doesn't digress into a conversation about `never`;
that isn't my point. I'm trying to provide more justification about
when I think attributes are appropriate, because I think they may be
appropriate here and I think it's useful to show how `never` is
different.

---

## #114127 — olleharstedt@gmail.com — 2021-04-24T15:52:20+00:00

URL: https://externals.io/message/114116#114127 · In reply to: #114119

2021-04-24 12:56 GMT, Pierre <pierre-php@processus.org>:
> Le 24/04/2021 à 12:55, Saif Eddin Gmati a écrit :
> > […]

1) Doesn't this violate the principle: It should be possible to add
new features without touching old code?

2) Isn't namespace-internal access a better feature for the same
purpose? That is, only allows a class to be extended within the same
namespace.

Olle

---

## #114128 — Saif Eddin Gmati — 2021-04-24T15:59:02+00:00

URL: https://externals.io/message/114116#114128 · In reply to: #114127

> Doesn't this violate the principle: It should be possible to add new features without touching old code? 



This depends on which syntax is picked, both `for` and attribute syntax will be completely BC.



using `sealed`+`permits` or `permits` only will result in these keywords being reserved, so classes named `Sealed` or `Permits` will not work in PHP 8.1.

 

> Isn't namespace-internal access a better feature for the same purpose? That is, only allows a class to be extended within the same namespace. 



No, this is a different feature, sealed classes can permit classes in a completely different namespace, and restrict inheritance to a per-defined list of classes.



If we would allow inheritance in the same namespace, it's extremely easy to get around it:



```

namespace Lib {

  abstract class MyProxy implements PrivateFooInterface {}

}



namespace App {

  class MyFooImplementation extends \Lib\MyProxy {

    // ...

  }

}

```



while it is possible to get around sealed, or even final keywords ( https://github.com/dg/bypass-finals/blob/master/src/BypassFinals.php ), bypassing private classes/interface is much easier, and doesn't require any hacks.











---- On Sat, 24 Apr 2021 16:52:20 +0100 Olle Härstedt <olleharstedt@gmail.com> wrote ----



2021-04-24 12:56 GMT, Pierre <mailto:pierre-php@processus.org>: 
> Le 24/04/2021 à 12:55, Saif Eddin Gmati a écrit : 
> > […]
 
1) Doesn't this violate the principle: It should be possible to add 
new features without touching old code? 
 
2) Isn't namespace-internal access a better feature for the same 
purpose? That is, only allows a class to be extended within the same 
namespace. 
 
Olle 
 
-- 
 
To unsubscribe, visit: https://www.php.net/unsub.php

---

## #114129 — Larry Garfield — 2021-04-24T16:42:48+00:00

URL: https://externals.io/message/114116#114129 · In reply to: #114117

> A major behavior i wanted to discuss is how should sealed interfaces 
> work, and specially around `Throwable` which is currently sealed to 
> only `Error` and `Exception`, but allows other interfaces to extend it, 
> and other classes to implement it as long as they extend `Error` or 
> `Exception` ( or another class that does so ).
> 
> As per the current proposal, if `Throwable` is to be declared `sealed`, 
> permitting only `Error` and `Exception`, it won't be possible to extend 
> it, resulting in a major BC-break, considering that too many libraries 
> extend this interface in their own `ExceptionInterface`.
> 
> To work around this, there's two solution:
> 
> 1. don't declare `Throwable` sealed and keep it as a special case.
> 2. interfaces declared sealed, can be extended by other interfaces, 
> however, classes that implement either the sealed interface directly, 
> or via another interface, have to extend one of the classes the sealed 
> interface permits. 
> 
> The second solution will allow declaring `Throwable` sealed, and would 
> bring complete consistency between internally sealed symbols, and user 
> land, without any BC breaks, and IMHO, it doesn't hurt as at the end, 
> no one will be able to implement `Throwable` without extending the 
> permitted classes. 
> 
> Note: with the second solution, if an interface is sealed and permits 
> only 2 classes which are final, and you extend that interface into 
> another, there's no way you can actually implement your new interface, 
> and i will be as if you just declared a final interface.
> 
> 
> ‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐
> On Saturday, April 24, 2021 11:55 AM, Saif Eddin Gmati 
> <azjezz@protonmail.com> wrote:
> 
> > […]

1) I am generally supportive of this concept.

2) For exceptions, I'm honestly fine with Throwable just being weird.  It already is, so letting it stay weird doesn't hurt anything.

3) The syntax examples mention traits, but nothing else in the spec talks about traits.  What does sealed mean on a trait?  I assume "can only be 'use'd by these classes", but that should be explicit.

4) I'm on team Attributes for the syntax, for two main reasons.  One, it eliminates the need for a new keyword entirely.  It would only require defining a new built-in class, and that class could be namespaced (per the likely-to-pass namespace policy) so that it is unlikely to conflict with anything at all.  Two, I find it visually better as it it is not part of the type system per se, but a sort of meta-type system.  Which is exactly where Attributes fit.  (Basically what Levi said.)

5) If this passes, then combined with Nikita's "new in intializiers" RFC (https://wiki.php.net/rfc/new_in_initializers) it would effectively give us ADTs/Union Types by a different syntax.  One could argue which syntax is better or more convenient (I'm not sure which I favor myself), but I'm just calling out that this would give us effectively the same result in terms of capability.

--Larry Garfield

---

## #114136 — olleharstedt@gmail.com — 2021-04-24T19:44:07+00:00

URL: https://externals.io/message/114116#114136 · In reply to: #114128

2021-04-24 17:59 GMT+02:00, Saif Eddin Gmati <azjezz@void.tn>:
> > […]
>
> This depends on which syntax is picked, both `for` and attribute syntax will
> be completely BC.

I'm not talking about BC, but the maintainability of the new feature
itself. For the shape example, you'd need to edit the original file
for each new shape you add, which is detrimental for maintainability
and scalability. So what's a good use-case?

Olle

---

## #114137 — Marco Pivetta — 2021-04-24T19:51:50+00:00

URL: https://externals.io/message/114116#114137 · In reply to: #114136

> 2021-04-24 17:59 GMT+02:00, Saif Eddin Gmati <azjezz@void.tn>:
> > […]
> will
> > […]
>
> I'm not talking about BC, but the maintainability of the new feature
> itself. For the shape example, you'd need to edit the original file
> for each new shape you add, which is detrimental for maintainability
> and scalability. So what's a good use-case?
>

The main use-case of sealed types is being able to declare total functions
around them.

---

## #114138 — olleharstedt@gmail.com — 2021-04-24T19:55:16+00:00

URL: https://externals.io/message/114116#114138 · In reply to: #114137

2021-04-24 21:51 GMT+02:00, Marco Pivetta <ocramius@gmail.com>:
> 
>
> > […]
>
> The main use-case of sealed types is being able to declare total functions
> around them.

What is "total function" in your discourse? :) Can you find a more
concrete example? Preferably one that's relevant for web site/app
development. Shapes is a bit too generic, I think.

Olle

---

## #114139 — Christian Schneider — 2021-04-24T20:01:30+00:00

URL: https://externals.io/message/114116#114139 · In reply to: #114137

Am 24.04.2021 um 21:51 schrieb Marco Pivetta <ocramius@gmail.com>:
> 
> 
> > […]

I'm with Olle here: This sounds like an anti-pattern to me.
The example could not be worse: Why should I not be allowed to add a hexagon shape?

> The main use-case of sealed types is being able to declare total functions
> around them.

Could you elaborate on what's the real-world use-case for your main use-case?
This sounds like another case of a feature based in (math) theory which leads to artificially locked down code.

- Chris

---

## #114141 — David Gebler — 2021-04-24T23:39:47+00:00

URL: https://externals.io/message/114116#114141 · In reply to: #114139

I don't love this idea, I'm not very fond of the final keyword, either;
I've always believed annotations (or attributes in PHP these days) are a
better of way of indicating you, as an author of a class, did not write it
with inheritability in mind or intended than restricting language features
through syntactic constructs.

The RFC says "when you have a class in your code base that shares some
implementation detail between 2 or more other objects, your only protection
against others making use of this class is to add `@internal` annotation,
which doesn't offer any runtime guarantee that no one is extending this
object", to which I ask - why do you need this guarantee? What does it
qualitatively add? If I make a judgement that I want to extend your class
or implement your interface, I can just delete the sealed keyword from your
code and carry on. So it doesn't actually offer any guarantee at all that
I'm not extending the type. The best it can achieve is to indicate your
intentions, which I believe can be adequately done today through an
attribute, no addition to the language needed.

On Sat, Apr 24, 2021 at 9:01 PM Christian Schneider <cschneid@cschneid.com>
wrote:

> Am 24.04.2021 um 21:51 schrieb Marco Pivetta <ocramius@gmail.com>:
> > […]
> <mailto:olleharstedt@gmail.com>> wrote:
> > […]
>
> I'm with Olle here: This sounds like an anti-pattern to me.
> The example could not be worse: Why should I not be allowed to add a
> hexagon shape?
>
> > […]
> functions
> > […]
>
> Could you elaborate on what's the real-world use-case for your main
> use-case?
> This sounds like another case of a feature based in (math) theory which
> leads to artificially locked down code.
>
> - Chris

---

## #114142 — Larry Garfield — 2021-04-25T03:47:52+00:00

URL: https://externals.io/message/114116#114142 · In reply to: #114138

> 2021-04-24 21:51 GMT+02:00, Marco Pivetta <ocramius@gmail.com>:
> > […]
> 
> What is "total function" in your discourse? :) Can you find a more
> concrete example? Preferably one that's relevant for web site/app
> development. Shapes is a bit too generic, I think.
> 
> Olle

A total function is a function that is defined over the entire domain of its inputs.  For example, addition is a total function over integers, because for every possible pair of integers you pass to it there is a logical return value.  However, square root is not a total function over integers because there are some integers you pass it for which there is not representable return value.  (Negative numbers, unless you get into imaginary numbers which PHP doesn't support.)  In those cases, you have to throw an exception or return an error code or similar.

For a more typical PHP example, getUser(int $id) is not a total function, unless you have PHP_MAX_INT user objects defined in your database.  If you pass an int that does not correspond to a defined user, you now have to deal with "user not found" error handling.  getUser() is not a total function.  getUsers(array $criteria), however, arguably is, because it's logical and reasonable to map all not-found cases to an empty array/collection, which doesn't require any special error handling.

In practice, I think all of the use cases for sealed classes are ADT-esque.  As I noted before, combining sealed classes with Nikita's new-in-expressions RFC would allow for this (also using my short-functions RFC for this example, although that's a nice-to-have):

sealed class Maybe permits Some, None {

  public const None = new None();

  static public function Some($x) => new Some($x);

  public function `value()` => throw new NotFoundException();

  public function bind(callable $c) => static::None;
}

final class None extends Maybe {}

final class Some extends Maybe {
  private $val;
  private function __construct($x) { $this->val = $x; }

  public function `value()` => $this->val;

  public function bind(callable $c) => new static($c($this->val));
}

Now if you have an instance of Maybe, you can be absolutely guaranteed that it's either an instance of Some or of None.  It's very similar to the guarantee you get for enumerations, that you will have one of a fixed set of dev-defined values and don't need to worry about any other case.  You handle None, you handle Some, and now your function is a total function over its Maybe parameter.

There are assorted other cases along those lines.

That gets you essentially the same functionality by a different route as what the tagged unions RFC (https://wiki.php.net/rfc/tagged_unions) proposes:

enum Maybe {
  case None {
    public function bind(callable $f) => $this;
    }
  };
 
  case Some(private mixed $value) {
    public function bind(callable $f): Maybe => $f($this->value);
  };
 
  public function `value()`: mixed => $this instanceof None 
    ? throw new Exception()
    : $this->val;
}

Or to use another example from the tagged unions RFC:

enum Distance {
    case Kilometers(public int $km);
    case Miles(public int $miles);
}

vs:

sealed interface Distance permits Kilometers, Miles { ... }

class Kilometers implements Distance {
  public function __construct(public int $km) {}
}

class Miles implements Distance {
  public function __construct(public int $miles) {}
}

In either case, a function can now operate on distance and know that it's dealing with a value in miles OR in kilometers, but it doesn't have to worry about yards, furlongs, or light-years.  Combined with a pattern-matching operator (which Ilija is working on here: https://wiki.php.net/rfc/pattern-matching), it would make it possible to combine ADTs/sealed classes with a match() statement and know that you've covered every possible situation with a trivial amount of code.

Enums, Sealed classes, and tagged unions all play in the same logical space, of allowing the developer to more precisely define their problem space and data model in a way that "makes invalid states unrepresentable",,and thus eliminates a large amount of error handling resulting in code that is harder if not impossible to "get wrong."  

I think it's clear that there's desire to have such capability, but the specifics of how we get there are not as clear-cut.  For instance, as currently envisioned tagged unions would extend enums, and as they're a dedicated language construct we can build in read-only properties.  Sealed classes wouldn't be able to do that... however, if we also added asymmetric visibility to properties or Nikita's proposed property accessors, a class could itself force a property to be public-read-only.  At that point, you would be able to implement the entire tagged-union RFC's functionality by combining sealed classes, read-only properties, and pattern matching.  It would just have a less specific-to-the-use-case syntax, which could be good or bad depending on your point of view.

I hope that clears up the problem space this RFC is working in.  Whether we want to achieve that functionality through enum-based tagged unions or through sealed classes, new-in-expression, and read-only properties is an open question, and I'm not entirely sure yet which I favor.  I can see pros and cons to both approaches; I just know I really want at least one of them, in 8.1 if at all possible. :-)

--Larry Garfield

---

## #114143 — olleharstedt@gmail.com — 2021-04-25T07:04:06+00:00

URL: https://externals.io/message/114116#114143 · In reply to: #114142

> A total function is a function that is defined over the entire domain of its
> inputs.  For example, addition is a total function over integers, because
> for every possible pair of integers you pass to it there is a logical return
> value.  However, square root is not a total function over integers because
> there are some integers you pass it for which there is not representable
> return value.  (Negative numbers, unless you get into imaginary numbers
> which PHP doesn't support.)  In those cases, you have to throw an exception
> or return an error code or similar.

Maybe nitpicking, but PHP-land shouldn't make up their own
definitions: "A total function is a function that is defined for all
possible values of its input. That is, it terminates and returns a
value." https://softwareengineering.stackexchange.com/questions/334874/in-the-context-of-functional-programming-what-are-total-functions-and-partia

Which means a total function is guaranteed to not have any errors,
like exceptions or division by zero. Compare with languages F* or Koka
which support this notation.

I get your point tho. :)

Olle

---

## #114144 — olleharstedt@gmail.com — 2021-04-25T07:19:43+00:00

URL: https://externals.io/message/114116#114144 · In reply to: #114142

> For a more typical PHP example, getUser(int $id) is not a total function,
> unless you have PHP_MAX_INT user objects defined in your database.  If you
> pass an int that does not correspond to a defined user, you now have to deal
> with "user not found" error handling.  getUser() is not a total function.
> getUsers(array $criteria), however, arguably is, because it's logical and
> reasonable to map all not-found cases to an empty array/collection, which
> doesn't require any special error handling.

getUser() could return a nullable type. But totality assumes purity,
and I'm assuming you didn't hard-code all users inside the getUser()
function. :) As soon as you interact with the outside world, you can
have exception and errors, and thus you can't guarantee termination.
In any case, I think the concept of totality is pretty foreign to PHP,
and we can probably leave it behind (you'd have to make sure -
statically, in the type-system - there are no infinite recursion, no
infinite loops, ...).

Olle

---

## #114145 — olleharstedt@gmail.com — 2021-04-25T07:39:37+00:00

URL: https://externals.io/message/114116#114145 · In reply to: #114142

> In practice, I think all of the use cases for sealed classes are ADT-esque.
> As I noted before, combining sealed classes with Nikita's new-in-expressions
> RFC would allow for this (also using my short-functions RFC for this
> example, although that's a nice-to-have):
>
> sealed class Maybe permits Some, None {
>
>   public const None = new None();
>
>   static public function Some($x) => new Some($x);
>
>   public function `value()` => throw new NotFoundException();
>
>   public function bind(callable $c) => static::None;
> }
>
> final class None extends Maybe {}
>
> final class Some extends Maybe {
>   private $val;
>   private function __construct($x) { $this->val = $x; }
>
>   public function `value()` => $this->val;
>
>   public function bind(callable $c) => new static($c($this->val));
> }

Yes, the Maybe/Option type is a good example! Because you know there
will never be another extension. But it's worth noting that whenever
you do *not* know that, these concepts suffer, even in functional
programming, by the same issues as I mentioned before with regard to
maintainability - you can't easily extend it without touching old code
(there are attempts to fix this by making algebraic datatypes
extensible, but it didn't get widely adopted AFAIK). Also see this
thread about the expression problem:
https://stackoverflow.com/a/871375/2138090

Disregarding the limitations of maintainability, the real power of
algebraic datatypes is of course the pattern matching functionality
seen in OCaml and Haskell. I'm leaning towards tagged unions + pattern
matching have more to offer PHP than sealed classes (and even more so
when pattern matching can be extended with guard clauses and catching
exceptions). The RFC author(s) might want to extend the RFC to reflect
the relation to tagged unions, and how they overlap (or not)?

Olle

---

## #114146 — Saif Eddin Gmati — 2021-04-25T14:43:12+00:00

URL: https://externals.io/message/114116#114146 · In reply to: #114145

---- On Sun, 25 Apr 2021 08:39:37 +0100 Olle Härstedt <olleharstedt@gmail.com> wrote ----

> > […]
 >  
 > Yes, the Maybe/Option type is a good example! Because you know there 
 > will never be another extension. But it's worth noting that whenever 
 > you do *not* know that, these concepts suffer, even in functional 
 > programming, by the same issues as I mentioned before with regard to 
 > maintainability - you can't easily extend it without touching old code 
 > (there are attempts to fix this by making algebraic datatypes 
 > extensible, but it didn't get widely adopted AFAIK). Also see this 
 > thread about the expression problem: 
 > https://stackoverflow.com/a/871375/2138090 
 >  
 > Disregarding the limitations of maintainability, the real power of 
 > algebraic datatypes is of course the pattern matching functionality 
 > seen in OCaml and Haskell. I'm leaning towards tagged unions + pattern 
 > matching have more to offer PHP than sealed classes (and even more so 
 > when pattern matching can be extended with guard clauses and catching 
 > exceptions). The RFC author(s) might want to extend the RFC to reflect 
 > the relation to tagged unions, and how they overlap (or not)? 
 >  
 > Olle 
 >  
 > -- 
 >  
 > To unsubscribe, visit: https://www.php.net/unsub.php 
 >  
 > 

As mentioned in a previous email ( https://news-web.php.net/php.internals/114134 ), there's many differences between ADTs and Sealed classes,
and in my opinion that's enough to have them both in the language as personally i have use cases where ADTs won't work.

examples:
- `sealed class UnionType permits ArrayKeyType, NumType` ( https://github.com/azjezz/psl/tree/1.7.x/src/Psl/Type/Internal )
- `sealed interface ExceptionInterface permits Exception` ( https://github.com/azjezz/psl/blob/1.7.x/src/Psl/Type/Exception ) - applies to all components in library
- `sealed class Exception permits AssertException, CoercionException` ( https://github.com/azjezz/psl/blob/1.7.x/src/Psl/Type/Exception )
- `sealed class ResourceHandle permits Psl\Filesystem\Internal\ResourceFileHandle` ( https://github.com/azjezz/psl/blob/asio/src/Psl/IO/Internal / https://github.com/azjezz/psl/tree/asio/src/Psl/Filesystem/Internal )

This is just a list of top of my head, the protection `sealed` offers is at the same level of `final`, expect it allow me to use inheritance internally without allowing the end users to do so.

---

## #114147 — Mike Schinkel — 2021-04-25T14:48:03+00:00

URL: https://externals.io/message/114116#114147 · In reply to: #114142

> 
> 
> 
> > […]
> 
> A total function is a function that is defined over the entire domain of its inputs.  For example, addition is a total function over integers, because for every possible pair of integers you pass to it there is a logical return value.  However, square root is not a total function over integers because there are some integers you pass it for which there is not representable return value.  (Negative numbers, unless you get into imaginary numbers which PHP doesn't support.)  In those cases, you have to throw an exception or return an error code or similar.
> 
> For a more typical PHP example, getUser(int $id) is not a total function, unless you have PHP_MAX_INT user objects defined in your database.  If you pass an int that does not correspond to a defined user, you now have to deal with "user not found" error handling.  getUser() is not a total function.  getUsers(array $criteria), however, arguably is, because it's logical and reasonable to map all not-found cases to an empty array/collection, which doesn't require any special error handling.
> 
> In practice, I think all of the use cases for sealed classes are ADT-esque.  As I noted before, combining sealed classes with Nikita's new-in-expressions RFC would allow for this (also using my short-functions RFC for this example, although that's a nice-to-have):
> 
> sealed class Maybe permits Some, None {
> 
>  public const None = new None();
> 
>  static public function Some($x) => new Some($x);
> 
>  public function `value()` => throw new NotFoundException();
> 
>  public function bind(callable $c) => static::None;
> }
> 
> final class None extends Maybe {}
> 
> final class Some extends Maybe {
>  private $val;
>  private function __construct($x) { $this->val = $x; }
> 
>  public function `value()` => $this->val;
> 
>  public function bind(callable $c) => new static($c($this->val));
> }
> 
> Now if you have an instance of Maybe, you can be absolutely guaranteed that it's either an instance of Some or of None.  It's very similar to the guarantee you get for enumerations, that you will have one of a fixed set of dev-defined values and don't need to worry about any other case.  You handle None, you handle Some, and now your function is a total function over its Maybe parameter.
> 
> There are assorted other cases along those lines.
> 
> That gets you essentially the same functionality by a different route as what the tagged unions RFC (https://wiki.php.net/rfc/tagged_unions) proposes:
> 
> enum Maybe {
>  case None {
>    public function bind(callable $f) => $this;
>    }
>  };
> 
>  case Some(private mixed $value) {
>    public function bind(callable $f): Maybe => $f($this->value);
>  };
> 
>  public function `value()`: mixed => $this instanceof None 
>    ? throw new Exception()
>    : $this->val;
> }
> 
> Or to use another example from the tagged unions RFC:
> 
> enum Distance {
>    case Kilometers(public int $km);
>    case Miles(public int $miles);
> }
> 
> vs:
> 
> sealed interface Distance permits Kilometers, Miles { ... }
> 
> class Kilometers implements Distance {
>  public function __construct(public int $km) {}
> }
> 
> class Miles implements Distance {
>  public function __construct(public int $miles) {}
> }
> 
> In either case, a function can now operate on distance and know that it's dealing with a value in miles OR in kilometers, but it doesn't have to worry about yards, furlongs, or light-years.  Combined with a pattern-matching operator (which Ilija is working on here: https://wiki.php.net/rfc/pattern-matching), it would make it possible to combine ADTs/sealed classes with a match() statement and know that you've covered every possible situation with a trivial amount of code.
> 
> Enums, Sealed classes, and tagged unions all play in the same logical space, of allowing the developer to more precisely define their problem space and data model in a way that "makes invalid states unrepresentable",,and thus eliminates a large amount of error handling resulting in code that is harder if not impossible to "get wrong."


Nothing I am about to write is meant to question the value of total functions, but can you speak to the ramifications of a developer using a library with a total function that returns miles or kilometers in (say) version 2.0, along with the assumed guarantees of said function, but then in version 3.0 the developer adds furlongs as a unit of measure?

Yes going from 2.0 to 3.0 is a breaking change per semver, but it doesn't feel like it had to be had the application developer not made the total function assumption.

-Mike

> 
> I think it's clear that there's desire to have such capability, but the specifics of how we get there are not as clear-cut.  For instance, as currently envisioned tagged unions would extend enums, and as they're a dedicated language construct we can build in read-only properties.  Sealed classes wouldn't be able to do that... however, if we also added asymmetric visibility to properties or Nikita's proposed property accessors, a class could itself force a property to be public-read-only.  At that point, you would be able to implement the entire tagged-union RFC's functionality by combining sealed classes, read-only properties, and pattern matching.  It would just have a less specific-to-the-use-case syntax, which could be good or bad depending on your point of view.
> 
> I hope that clears up the problem space this RFC is working in.  Whether we want to achieve that functionality through enum-based tagged unions or through sealed classes, new-in-expression, and read-only properties is an open question, and I'm not entirely sure yet which I favor.  I can see pros and cons to both approaches; I just know I really want at least one of them, in 8.1 if at all possible. :-)
> 
> --Larry Garfield
> 
> --
> 
> To unsubscribe, visit: https://www.php.net/unsub.php

---

## #114149 — Mike Schinkel — 2021-04-25T15:36:24+00:00

URL: https://externals.io/message/114116#114149 · In reply to: #114141

> 
> 
> I don't love this idea, I'm not very fond of the final keyword, either;

I'll start by saying the final keyword caused me a tremendous amount of heartache because it was used on a class in a framework that I badly, badly needed to extend.

But even so, I recognize why they used it, and I still don't have a great argument for how they could address the reasons they used it some other way.

> I've always believed annotations (or attributes in PHP these days) are a
> better of way of indicating you, as an author of a class, did not write it
> with inheritability in mind or intended than restricting language features
> through syntactic constructs.
> 
> The RFC says "when you have a class in your code base that shares some
> implementation detail between 2 or more other objects, your only protection
> against others making use of this class is to add `@internal` annotation,
> which doesn't offer any runtime guarantee that no one is extending this
> object", to which I ask - why do you need this guarantee? What does it
> qualitatively add? If I make a judgement that I want to extend your class
> or implement your interface, I can just delete the sealed keyword from your
> code and carry on. So it doesn't actually offer any guarantee at all that
> I'm not extending the type.

Actually, it does offer such a guarantee.  It guarantees if you are using a non-forked version of the original developer's (OD's) library or framework then that class won't be extended. When someone pulls the original non-forked version from its source repository — such as when using Composer — then that code will be (effectively) guaranteed not to be extended.

OTOH, if you do delete the sealed (or final) keyword you have then forked the code, in a defacto manner if not a literal one. If you use a forked version of the code, you now own the maintenance of that code and any bugs that are generated by your forked changes in using code. The original developer has no moral, ethical or even contractual obligation to care about the breakage you cause.

Hypothetical example:  You fork the code, remove sealed/final, then subclass the code and add a method, let's call it ToString(). And you write your application to use ToString(). Now the OD releases a new minor version and they also add a ToString() method. Applications using your fork probably cannot use the new version of the OD's library because when the library calls ToString() your version is called. So you have to update your application to use the new version of the library and once again remove sealed/final.

AND, if your code is instead another add-on library, now users of your add-on library will also have to fix their code too.  Which could potentially be a large number of users if your add-on is successful.

So not using final or sealed can result in some really hairy and possibly impossible to fully resolve backward compatibility concerns for developers who publish libraries and/or frameworks.

> The best it can achieve is to indicate your
> intentions, which I believe can be adequately done today through an
> attribute, no addition to the language needed.

Still, I concur with your concerns.  Developers too often implement final classes in libraries and frameworks without fully addressing all the use-cases and/or adding enough extensibility points because it makes their lives easier.  Because of that final — and sealed, if added — can make the life of an application developer a living hell.

So what's the answer?  I don't know that I have the ultimate answer, but I would be a lot more comfortable with adding features to PHP such as ones like sealed that restrict the "O" in S.O.L.I.D.[0] if PHP were to offer the following three (3) things, all of which can be found in Go, and I am sure other languages:

1. Class embedding[1] — Allows one class to embed another and immediately have access to all its properties and methods, and also to be able to extract an instance of that embedded class.  It is called "Type embedding" in Go.

2.Type definitions[2] — A typedef would allow developers to define constrained versions of existing types, such as `FiveStarRating` which could only contain 1, 2, 3, 4 or 5, or types that identify a signature, for example as `ConvertToString` which could require a closure that implements `func(mixed):string`. In Go you can compose other types to create new types, but I'm not sure if those other type of types could apply to PHP, at least as it currently exists, and especially because it is not a compiled language.

3. Structural typing[3] — Basically interfaces that can be implemented implicitly rather than explicitly.  For example, if I wanted to implement a Stringable interface that requires a ToString():string method then structural typing would allow me to implement that interface simply by adding a ToString() method instead of requiring me to also add "implements Stringable" to the class definition. 

Those three features are all killer language features and would make great additions to PHP.  IMO, of course.

#fwiw

-Mike

[0] https://stackify.com/solid-design-open-closed-principle/
[1] https://travix.io/type-embedding-in-go-ba40dd4264df
[2] https://go101.org/article/type-system-overview.html
[3] https://blog.carbonfive.com/structural-typing-compile-time-duck-typing/ <https://blog.carbonfive.com/structural-typing-compile-time-duck-typing/

---

## #114150 — Larry Garfield — 2021-04-25T16:24:25+00:00

URL: https://externals.io/message/114116#114150 · In reply to: #114147

> > […]

> Nothing I am about to write is meant to question the value of total 
> functions, but can you speak to the ramifications of a developer using 
> a library with a total function that returns miles or kilometers in 
> (say) version 2.0, along with the assumed guarantees of said function, 
> but then in version 3.0 the developer adds furlongs as a unit of 
> measure?
> 
> Yes going from 2.0 to 3.0 is a breaking change per semver, but it 
> doesn't feel like it had to be had the application developer not made 
> the total function assumption.
> 
> -Mike

It would be a breaking change, because you're changing the definition of the problem space.  Just as if you added a new option to an enum, or changed an Interface to have a method return iterable instead of array.  If you change the definition of the data model, that's a breaking change and all bets are off anyway.

Whether or not making it sealed in the first place was a good call is going to vary widely depending on the situation.  They're really not appropriate for service objects, where by design you could have an infinite number of possible PaymentGateway objects.  They're much more applicable for data objects that represent a finite number of business-relevant situations, where changing them means the problem space is changing *anyway* so changing code around the problem space is going to happen anyway.

--Larry Garfield

---

## #114151 — Larry Garfield — 2021-04-25T16:27:14+00:00

URL: https://externals.io/message/114116#114151 · In reply to: #114143

> > […]
> 
> Maybe nitpicking, but PHP-land shouldn't make up their own
> definitions: "A total function is a function that is defined for all
> possible values of its input. That is, it terminates and returns a
> value." 
> https://softwareengineering.stackexchange.com/questions/334874/in-the-context-of-functional-programming-what-are-total-functions-and-partia
> 
> Which means a total function is guaranteed to not have any errors,
> like exceptions or division by zero. Compare with languages F* or Koka
> which support this notation.

That... is literally what I said.  There was no making up definitions.  I was using the actual mathematical definition.  PHP is quite capable of having total functions, they're a good thing, and we should try to encourage them where feasible.

--Larry Garfield

---

## #114152 — olleharstedt@gmail.com — 2021-04-25T16:34:05+00:00

URL: https://externals.io/message/114116#114152 · In reply to: #114151

2021-04-25 18:27 GMT+02:00, Larry Garfield <larry@garfieldtech.com>:
> 
> > […]
>
> That... is literally what I said.  There was no making up definitions.  I
> was using the actual mathematical definition.  PHP is quite capable of
> having total functions, they're a good thing, and we should try to encourage
> them where feasible.

What's the point of encouraging total functions if you can't express
the totality in the type system?

Olle

---

## #114153 — Larry Garfield — 2021-04-25T16:45:42+00:00

URL: https://externals.io/message/114116#114153 · In reply to: #114146

> ---- On Sun, 25 Apr 2021 08:39:37 +0100 Olle Härstedt 
> <olleharstedt@gmail.com> wrote ----
> 
> > […]
> ADT-esque. 
> > […]
> new-in-expressions 
> > […]
> this 
> > […]
> code 
> > […]
> pattern 
> > […]
> so 
> > […]
> catching 
> > […]
> reflect 
> > […]

Pattern matching as it's currently being worked on would apply to arbitrary objects with visible properties, and enums are, in the engine, "just objects."  So both a sealed class with public properties (or rather, properties visible in the scope) and an Enum with properties added (tagged unions/ADTs) would look the same to pattern matching.  Naturally that RFC has to get finished first, but assuming it is, it would apply either way.

(Discrete but complementary components FTW.)

> As mentioned in a previous email ( 
> https://news-web.php.net/php.internals/114134 ), there's many 
> differences between ADTs and Sealed classes,
> and in my opinion that's enough to have them both in the language as 
> personally i have use cases where ADTs won't work.

Many differences in terms of their implementation details, yes.  Ilija lays out a good list of those in that email.  However, they do address the same conceptual problem space.

Is there anything that ADTs or sealed classes would enable that the other one simply couldn't?  If we can get a list of those, that can help us determine which is the ideal route to pursue or if there really is value in adding both despite their heavy overlap.

(The list of links you had before lacks enough context for me to figure out what you're suggesting, sorry.  Please provide more precise comparisons of the two here.)

----------

Also, another syntax suggestion:

final class Foo permits Bar, Baz {}

class Bar extends Foo {}

`final` already indicates that a class is inextensible.  Sealed classes are poking holes in that guard.  Essentially, final on its own is an extension cardinality of 0, sealed classes offer an exception list to that to give a precise cardinality list.

The main advantage here is that it introduces only a single new keyword, not multiple.  If `permits` were replaced with `for` (as the RFC already offers), it would be no new keywords.  Minimizing new keywords is generally a good idea anyway.

---

## #114154 — michal.brzuchalski@gmail.com — 2021-04-25T16:58:48+00:00

URL: https://externals.io/message/114116#114154 · In reply to: #114117

Hello Saif

sob., 24 kwi 2021, 13:00 użytkownik Saif Eddin Gmati <azjezz@protonmail.com>
napisał:

> A major behavior i wanted to discuss is how should sealed interfaces work,
> and specially around `Throwable` which is currently sealed to only `Error`
> and `Exception`, but allows other interfaces to extend it, and other
> classes to implement it as long as they extend `Error` or `Exception` ( or
> another class that does so ).
>
> As per the current proposal, if `Throwable` is to be declared `sealed`,
> permitting only `Error` and `Exception`, it won't be possible to extend it,
> resulting in a major BC-break, considering that too many libraries extend
> this interface in their own `ExceptionInterface`.
>
> To work around this, there's two solution:
>
> 1. don't declare `Throwable` sealed and keep it as a special case.
> 2. interfaces declared sealed, can be extended by other interfaces,
> however, classes that implement either the sealed interface directly, or
> via another interface, have to extend one of the classes the sealed
> interface permits.
>
> The second solution will allow declaring `Throwable` sealed, and would
> bring complete consistency between internally sealed symbols, and user
> land, without any BC breaks, and IMHO, it doesn't hurt as at the end, no
> one will be able to implement `Throwable` without extending the permitted
> classes.
>

I like the idea of sealed class along with proposed keywords which are
similar to already existing in other languages like Java.

Personally I'd go with the second option.

Speaking of Attributes I prefer not to use an Attribute for any particular
language feature which expects input arguments to be a valid class or
interface name for two reasons: first because there is no effective way to
restrict input string to be a valid class or interface name and second that
it'd require passing strings which means in most cases passing class or
interface name with magic ::class constant read.

Cheers,
Michał Marcin Brzuchalski

---

Next page: https://externals.io/message/114116.md?page=2
