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.
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:
- don't declare
Throwable
sealed and keep it as a special case. - 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.
A major behavior i wanted to discuss is how should sealed interfaces
work, and specially aroundThrowable
which is currently sealed to
onlyError
andException
, but allows other interfaces to extend it,
and other classes to implement it as long as they extendError
or
Exception
( or another class that does so ).As per the current proposal, if
Throwable
is to be declaredsealed
,
permitting onlyError
andException
, it won't be possible to extend
it, resulting in a major BC-break, considering that too many libraries
extend this interface in their ownExceptionInterface
.To work around this, there's two solution:
- don't declare
Throwable
sealed and keep it as a special case.- 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 implementThrowable
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: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.
-
I am generally supportive of this concept.
-
For exceptions, I'm honestly fine with Throwable just being weird. It already is, so letting it stay weird doesn't hurt anything.
-
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.
-
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.)
-
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
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 aroundThrowable
which is currently sealed to onlyError
andException
, but allows other interfaces to extend it, and other
classes to implement it as long as they extendError
orException
( or
another class that does so ).As per the current proposal, if
Throwable
is to be declaredsealed
,
permitting onlyError
andException
, it won't be possible to extend it,
resulting in a major BC-break, considering that too many libraries extend
this interface in their ownExceptionInterface
.To work around this, there's two solution:
- don't declare
Throwable
sealed and keep it as a special case.- 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 implementThrowable
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
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_classesA 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
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_classesA 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 ?
+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
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_classesA 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 ?
+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.
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.
I think I disagree with this very strongly, and plan* to vote against
any RFC that embeds another language in annotations.**
It might be quicker, easier, and more seductive to implement language
level features in them, but it is a massive trade-off in making code
hard to reason about.
That should increase the likeliness of it
getting accepted in my opinion.
This can also be pronounced as "makes it more likely to slip bad ideas
in core without thinking them through fully".
My experience of annotations being used as an embedded language is
Java, and it's one of the reasons why I no longer use that language.
Not only is code with many lines of annotations hard to read, it
results in behaviour that is very hard to reason about. I literally
spent 40 hours (spread over the course of 5 weeks), trying to figure
out this bug https://stackoverflow.com/q/9072749/778719 . If my
colleague hadn't been able to tell me the answer, the only way I could
have debugged the problem myself is using a bytecode level debugger,
to step through the internal details of what was happening.
I'd prefer it if we didn't repeat (what I consider to be) the mistakes of Java.
cheers
Dan
Ack
- possibly with the exception of optimization annotations e.g. 'memoize'.
** "From a language design perspective, annotations form a
mini-language embedded in Java" -
https://blog.softwaremill.com/the-case-against-annotations-4b2fb170ed67
Though probably most of the other links from "java annotations are
crap" are also appropriate.
Le 27/04/2021 à 15:21, Dan Ackroyd a écrit :
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.
I think I disagree with this very strongly, and plan* to vote against
any RFC that embeds another language in annotations.**It might be quicker, easier, and more seductive to implement language
level features in them, but it is a massive trade-off in making code
hard to reason about.That should increase the likeliness of it
getting accepted in my opinion.
This can also be pronounced as "makes it more likely to slip bad ideas
in core without thinking them through fully".My experience of annotations being used as an embedded language is
Java, and it's one of the reasons why I no longer use that language.Not only is code with many lines of annotations hard to read, it
results in behaviour that is very hard to reason about. I literally
spent 40 hours (spread over the course of 5 weeks), trying to figure
out this bug https://stackoverflow.com/q/9072749/778719 . If my
colleague hadn't been able to tell me the answer, the only way I could
have debugged the problem myself is using a bytecode level debugger,
to step through the internal details of what was happening.I'd prefer it if we didn't repeat (what I consider to be) the mistakes of Java.
cheers
Dan
Ack
Hello,
Yes, after reading a few answers I changed my mind and I do agree with
you know, this is a language feature and should be in the language
syntax and not in attributes.
Regards,
--
Pierre
2021-04-24 12:56 GMT, Pierre pierre-php@processus.org:
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_classesA 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.
-
Doesn't this violate the principle: It should be possible to add
new features without touching old code? -
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
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 :
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_classesA 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.
-
Doesn't this violate the principle: It should be possible to add
new features without touching old code? -
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
2021-04-24 17:59 GMT+02:00, Saif Eddin Gmati azjezz@void.tn:
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.
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
2021-04-24 17:59 GMT+02:00, Saif Eddin Gmati azjezz@void.tn:
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.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.
2021-04-24 21:51 GMT+02:00, Marco Pivetta ocramius@gmail.com:
2021-04-24 17:59 GMT+02:00, Saif Eddin Gmati azjezz@void.tn:
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.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.
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
2021-04-24 21:51 GMT+02:00, Marco Pivetta ocramius@gmail.com:
2021-04-24 17:59 GMT+02:00, Saif Eddin Gmati azjezz@void.tn:
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.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.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
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
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-partiaWhich 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
2021-04-25 18:27 GMT+02:00, Larry Garfield larry@garfieldtech.com:
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-partiaWhich 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.
What's the point of encouraging total functions if you can't express
the totality in the type system?
Olle
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
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
---- On Sun, 25 Apr 2021 08:39:37 +0100 Olle Härstedt olleharstedt@gmail.com wrote ----
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/2138090Disregarding 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.
---- On Sun, 25 Apr 2021 08:39:37 +0100 Olle Härstedt
olleharstedt@gmail.com wrote ----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/2138090Disregarding 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
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.
2021-04-24 21:51 GMT+02:00, Marco Pivetta ocramius@gmail.com:
2021-04-24 17:59 GMT+02:00, Saif Eddin Gmati azjezz@void.tn:
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.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.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."
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
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.
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
Am 25.04.2021 um 05:47 schrieb Larry Garfield larry@garfieldtech.com:
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 {
...
}
final class None extends Maybe {}
This is exactly the thing I'm worried about.
Say I want to add something like logging to the None type.
Now your sealed and final classes prevent me from defining MyNone extending None even though it would be 100% compatible with None. Just because you deemed that useless or wrong.
I've encountered situations like this and came to the conclusion that while this makes sense for languages like Haskell - where the whole idea is to be able to reason about a complex type system - it is an anti-pattern for other languages like PHP.
Referring to another post, not yours: People, please don't use Java as a reason to add something to PHP, Java is the king of anti-patterns ;-)
- Chris
Yes I agree Chris, this is the same kind of argument I am making.
Note that the exact same argument could be made for typing parameters.
I can document via a docblock that I expect a given parameter to be a
string, or a Request object, or whatever. "There is little to no benefit
in expressing that through a new language construct rather than through
existing constructs and design patterns."
Yes and no. There is a critical difference between type checking and sealed
- type checking can actually prevent and catch bugs in the form of
objective logical errors (e.g. $total = getSum(1, 2, "not a number"))
before they occur, whereas sealed can only prevent things you did not
intend or foresee, without knowing anything about how I am using some code;
there is no bug or logical error which the use of sealed will prevent in
and of itself.
Except there very much is a benefit, for making the intent of code
clearer and for allowing the engine to syntactically make certain invalid
states impossible.
This is not runtime benefit, it is runtime overhead. PHP cannot generate a
better opcode sequence for knowing a class is sealed. In terms of
implementation, the only thing it can do is scream "Hey! The author of this
class didn't intend for you to use it this way!" - your IDE can do that
from an attribute.
And although I would say the claim no one will ever have a legitimate &
good reason to extend some class and be able to do so in a way which does
not violate whatever protections against invalid state you had in mind is a
very bold prediction which is rarely borne out by reality - my objection
here is not about people marking classes as sealed, it's that we don't need
a new language construct and keyword to achieve the only benefit it
delivers (declaration of intent to users and IDE). If PHP reasoned about
classes in the same way compiled, statically typed languages do and
therefore derived some tangible compile-time benefit from a sealed
construct, I would be in favour of it.
On Mon, 26 Apr 2021, 08:54 Christian Schneider, cschneid@cschneid.com
wrote:
Am 25.04.2021 um 05:47 schrieb Larry Garfield larry@garfieldtech.com:
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 {
...
}
final class None extends Maybe {}
This is exactly the thing I'm worried about.
Say I want to add something like logging to the None type.
Now your sealed and final classes prevent me from defining MyNone
extending None even though it would be 100% compatible with None. Just
because you deemed that useless or wrong.I've encountered situations like this and came to the conclusion that
while this makes sense for languages like Haskell - where the whole idea is
to be able to reason about a complex type system - it is an anti-pattern
for other languages like PHP.Referring to another post, not yours: People, please don't use Java as a
reason to add something to PHP, Java is the king of anti-patterns ;-)
- Chris
Hi Christian
On Mon, Apr 26, 2021 at 9:54 AM Christian Schneider
cschneid@cschneid.com wrote:
Am 25.04.2021 um 05:47 schrieb Larry Garfield larry@garfieldtech.com:
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 {
...
}
final class None extends Maybe {}
This is exactly the thing I'm worried about.
Say I want to add something like logging to the None type.
Now your sealed and final classes prevent me from defining MyNone extending None even though it would be 100% compatible with None. Just because you deemed that useless or wrong.
The point of sealed type is to fix the number of subclasses a given
type can have, which means you can handle a value by type (as that
list is not finite). Code that handles Optional values could look like
this:
if ($option instanceof Option\None) {
throw new Exception();
}
// We now know the value is a Some
var_dump($option->value);
If you suddenly provide your own version of None the code above will
break. This is probably more obvious when you look at the
enum-equivalent.
enum Option {
case None;
case Some($value);
}
To most people it's obvious that you can't add new cases to an
existing enum. It doesn't sound sensible to add logging to a data
class. That's something that belongs into a hook or service of some
kind.
People might also want to use sealed for behavioral classes, the use
case here is the same as final. Final is not here to make your life
harder. It's here to make the lives of the library maintainers easier.
If they have to reason about every way a method could be overridden,
every change in the library would become more risky, require more
thought and more frequent major version updates. This means more work
and less features for you, too.
Ilija
Am 26.04.2021 um 14:18 schrieb Ilija Tovilo tovilo.ilija@gmail.com:
The point of sealed type is to fix the number of subclasses a given
type can have, which means you can handle a value by type (as that
list is not finite). Code that handles Optional values could look like
this:if ($option instanceof Option\None) { throw new Exception(); } // We now know the value is a Some var_dump($option->value);
I really hope that's not the way people will use the Maybe type....
If you suddenly provide your own version of None the code above will
break.
... but no, it wouldn't break, as MyNone extends None, so instanceof would still work.
To most people it's obvious that you can't add new cases to an
existing enum.
The RFC does not mention data classes (at least not explicitly), it talks about sharing common functionality.
So I think you're somewhat moving the goal posts.
If you want to limit Enums only then I would have to reconsider, but we were talking about generic classes AFAIK.
It doesn't sound sensible to add logging to a data
class. That's something that belongs into a hook or service of some
kind.
You're again making assumption about what's sensible for the user of a library, that's the mind-set I would like to avoid.
People might also want to use sealed for behavioral classes, the use
case here is the same as final. Final is not here to make your life
harder. It's here to make the lives of the library maintainers easier.
First of all: Yes, I'm no fan of final either.
I've encountered enough examples where it did make my life harder.
And as a library writer myself I value the user experience higher than the library developer work because there are (hopefully) a lot more users than developers.
If they have to reason about every way a method could be overridden,
every change in the library would become more risky, require more
thought and more frequent major version updates.
A properly designed API should be simple and stable enough to not need this kind of safe-guards.
Overly complicated, cathedral-style frameworks might yearn for it but I think it's addressing the symptoms, not the cause (-:C
This means more work and less features for you, too.
That kind of argument reminds me of people saying that user tracking is a good thing because I get to see more relevant advertisement.
You're a good sales-man though, I give you that, but you haven't convinced me yet ;-)
- Chris
Hi,
On Mon, Apr 26, 2021 at 9:54 AM Christian Schneider cschneid@cschneid.com
wrote:
Am 25.04.2021 um 05:47 schrieb Larry Garfield larry@garfieldtech.com:
...sealed class Maybe permits Some, None {
...
}final class None extends Maybe {}
This is exactly the thing I'm worried about.
Say I want to add something like logging to the None type.
Now your sealed and final classes prevent me from defining MyNone
extending None even though it would be 100% compatible with None.
I just want to note that this has nothing to do with Maybe made sealed
(which seems legit), only with None made final (which... could be debated,
but unrelated to the RFC at hand).
Regards,
--
Guilliam Xavier
Still, it remains that one could have a legitimate, justifiable reason to
extend Maybe / some other example, which was not foreseen by its author and
is prevented by sealed as a keyword despite the fact this inheritance,
correctly implemented, would not break anything, not define an impossible
state and not violate anything except the library author's own (limited)
imagination of how their code ought to be used.
Note in respect of the point people have raised about attributes and my own
previous comments, I am not advocating any Sealed attribute is added to the
language as a functional change or embedded language by the backdoor, I am
merely saying you as some class's author can add an attribute, right now,
to indicate that you consider the class sealed - and an IDE / automated
tooling could understand it and warn a user there is a risk to violating
it.
What's being proposed in the RFC is a functional change to the language
whereby attempting to extend a class designated as sealed to a
non-specified child results in a fatal error.
Now that is not a benefit in itself, it is merely a description of the
proposed change. My question is who or what benefits from this change? And
I look at this way:
-
The language engine doesn't benefit, since unlike some compiled
languages, there is no indication here it will result in improved opcode
sequences. -
The author of code doesn't benefit, to any extent greater than they
would by using an attribute or other metadata to indicate their intentions,
because if someone else comes along, installs their library and creates
problems for themselves by extending some concrete type in a manner which
was not intended, the author's obligation to them is zero. If improperly
inheriting or otherwise misusing code creates a problem for a consumer,
it's the consumer's mess to sort out. -
The consumer of code doesn't benefit, because if they didn't intend to
inherit from a sealed, concrete class, it being sealed makes no difference.
But if they did - and had a legitimate reason to do so - they are now
forced to fork, proxy or otherwise work around this artificial restriction
(which they can easily do, by the way, language keyword or none). In any
valid use case, this can distinctly and only be a disadvantage to them.
Someone - Larry I think - said something to the effect of this isn't about
restricting your users as an author from doing something you think they
shouldn't, and that it's about more completely defining problem spaces at
language level. I respectfully am not convinced. When we look at features
like enums and stronger typing, we can see distinct benefits in how as
users of PHP we can model better and write better, more effective and more
efficient code.
But in the case of sealed and the context of PHP, you cannot model your
problem space this way better than you can with an attribute or other means
of annotating metadata. I can certainly agree such metadata is an important
part and valuable tool for improved modeling, but making it a keyword and
throwing a fatal error if your limited intentions (which do not predict all
possible use cases) are violated is merely constraining users from being
able to do something for its own sake. While there is a legitimate argument
that serves to "syntactically make certain invalid states impossible", it
does the same for any valid states you didn't envision.
I'm maybe even inclined to suggest the desire to make a class sealed really
smells like you want to be using abstract classes and/or interfaces.
To the greatest extent which is practical, my preference is to enable other
developers rather than constrain them (subject to the caveat I will not
help them if they do something silly or irresponsible with my code on their
own volition) - I don't believe the "weirdness" exceptions PHP makes
internally for things like Throwable or Traversable are good reasons to
extend this to the language level.
On Tue, 27 Apr 2021, 16:05 Guilliam Xavier, guilliam.xavier@gmail.com
wrote:
Hi,
On Mon, Apr 26, 2021 at 9:54 AM Christian Schneider <cschneid@cschneid.com
wrote:
Am 25.04.2021 um 05:47 schrieb Larry Garfield larry@garfieldtech.com:
...sealed class Maybe permits Some, None {
...
}final class None extends Maybe {}
This is exactly the thing I'm worried about.
Say I want to add something like logging to the None type.
Now your sealed and final classes prevent me from defining MyNone
extending None even though it would be 100% compatible with None.I just want to note that this has nothing to do with Maybe made sealed
(which seems legit), only with None made final (which... could be debated,
but unrelated to the RFC at hand).Regards,
--
Guilliam Xavier
Still, it remains that one could have a legitimate, justifiable reason to
extend Maybe / some other example, which was not foreseen by its author and
is prevented by sealed as a keyword despite the fact this inheritance,
correctly implemented, would not break anything, not define an impossible
state and not violate anything except the library author's own (limited)
imagination of how their code ought to be used.Note in respect of the point people have raised about attributes and my own
previous comments, I am not advocating any Sealed attribute is added to the
language as a functional change or embedded language by the backdoor, I am
merely saying you as some class's author can add an attribute, right now,
to indicate that you consider the class sealed - and an IDE / automated
tooling could understand it and warn a user there is a risk to violating
it.What's being proposed in the RFC is a functional change to the language
whereby attempting to extend a class designated as sealed to a
non-specified child results in a fatal error.Now that is not a benefit in itself, it is merely a description of the
proposed change. My question is who or what benefits from this change? And
I look at this way:
The language engine doesn't benefit, since unlike some compiled
languages, there is no indication here it will result in improved opcode
sequences.The author of code doesn't benefit, to any extent greater than they
would by using an attribute or other metadata to indicate their intentions,
because if someone else comes along, installs their library and creates
problems for themselves by extending some concrete type in a manner which
was not intended, the author's obligation to them is zero. If improperly
inheriting or otherwise misusing code creates a problem for a consumer,
it's the consumer's mess to sort out.The consumer of code doesn't benefit, because if they didn't intend to
inherit from a sealed, concrete class, it being sealed makes no difference.
But if they did - and had a legitimate reason to do so - they are now
forced to fork, proxy or otherwise work around this artificial restriction
(which they can easily do, by the way, language keyword or none). In any
valid use case, this can distinctly and only be a disadvantage to them.Someone - Larry I think - said something to the effect of this isn't about
restricting your users as an author from doing something you think they
shouldn't, and that it's about more completely defining problem spaces at
language level. I respectfully am not convinced. When we look at features
like enums and stronger typing, we can see distinct benefits in how as
users of PHP we can model better and write better, more effective and more
efficient code.But in the case of sealed and the context of PHP, you cannot model your
problem space this way better than you can with an attribute or other means
of annotating metadata. I can certainly agree such metadata is an important
part and valuable tool for improved modeling, but making it a keyword and
throwing a fatal error if your limited intentions (which do not predict all
possible use cases) are violated is merely constraining users from being
able to do something for its own sake. While there is a legitimate argument
that serves to "syntactically make certain invalid states impossible", it
does the same for any valid states you didn't envision.I'm maybe even inclined to suggest the desire to make a class sealed really
smells like you want to be using abstract classes and/or interfaces.To the greatest extent which is practical, my preference is to enable other
developers rather than constrain them (subject to the caveat I will not
help them if they do something silly or irresponsible with my code on their
own volition) - I don't believe the "weirdness" exceptions PHP makes
internally for things like Throwable or Traversable are good reasons to
extend this to the language level.
I totally agree with everything said above.
I will add that while I don't support the idea of sealed classes in any
way, were it to be implemented, I agree that implementing it via
compiler/engine enforced attributes is not the way to do it, echoing Dan's
reasoning from earlier.
On Tue, 27 Apr 2021, 16:05 Guilliam Xavier, guilliam.xavier@gmail.com
wrote:Hi,
On Mon, Apr 26, 2021 at 9:54 AM Christian Schneider <
cschneid@cschneid.comwrote:
Am 25.04.2021 um 05:47 schrieb Larry Garfield <larry@garfieldtech.com
:
...sealed class Maybe permits Some, None {
...
}final class None extends Maybe {}
This is exactly the thing I'm worried about.
Say I want to add something like logging to the None type.
Now your sealed and final classes prevent me from defining MyNone
extending None even though it would be 100% compatible with None.I just want to note that this has nothing to do with Maybe made sealed
(which seems legit), only with None made final (which... could be
debated,
but unrelated to the RFC at hand).Regards,
--
Guilliam Xavier
--
Chase Peeler
chasepeeler@gmail.com
Le 27/04/2021 à 18:46, David Gebler a écrit :
What's being proposed in the RFC is a functional change to the language
whereby attempting to extend a class designated as sealed to a
non-specified child results in a fatal error.
It's not a functional change to the language, well, it is a new feature,
but it's actually not changing any paradigm in the language or the
engine. People will continue to write the same code, and people that
want to use it for some library or customer project internal purpose may
use it.
Now that is not a benefit in itself, it is merely a description of the
proposed change. My question is who or what benefits from this change? And
I look at this way:
The language engine doesn't benefit, since unlike some compiled
languages, there is no indication here it will result in improved opcode
sequences.The author of code doesn't benefit, to any extent greater than they
would by using an attribute or other metadata to indicate their intentions,
because if someone else comes along, installs their library and creates
problems for themselves by extending some concrete type in a manner which
was not intended, the author's obligation to them is zero. If improperly
inheriting or otherwise misusing code creates a problem for a consumer,
it's the consumer's mess to sort out.
I think that the debate "but if your seal your classes I won't be to
extend it" is overrated: it's not the language to chose whether or not
the library/software author can seal its classes or not, it's up the
library/software author to do its own choice. And in that regard, having
"sealed" classes in the language is actually bringing that possibility
to the library author, so it's more liberty.
By design, by convention, in any of a company or an open source software
community, it can be legit to explicitly hard-seal stuff, for blocking
users when they are doing it wrong, if for example by attempting to
inherit they will break other existing piece code, for example. There's
many scenarios where it's legit to seal classes, and it can be a good
practice also (depend on your practices I guess).
But if sealing means "you who want to change a behavior, you need to fix
other things deep in the code before being able to do that" then it's
good (and it's one of the use cases of sealing).
So each time someone use the "but we won't be able to legitimately
extend your class" he's basically saying that people that do put very
thorough and strict conventions in their own code are wrong by nature
and don't understand what is good. It's almost an insult - please don't
misunderstand what I say, I don't think at any moment that any of the
people here really meant that, but it it's fundamentally what this
argument does: it says that some conventions are by nature bad and the
language should enforce people not to write code like this.
I think the language should not enforce any practice, convention or code
style, it must remain neutral considering people's whereabouts,
workflows or practices. If one's want to seal his or her class, let him
or her do it.
I think this point/argument should be banned from this discussion.
Sealed classes are good under some conditions, maybe bad under others,
that's not the point, it's not about discussing each developer's,
community's or company's own conventions, but about is it OK technically
to add this feature to the language, and will it be or not a maintenance
burden, and finally will it actually break millions of lines of existing
code.
I'm inclined to say yes, it is good to add this feature, and I'm
skeptical about the fact that it would break existing code (except for
people that named their classes "Permits"), there's many use cases I
wish I had it in the past. I can live with it, but sometime, it could be
the right tool.
Regards,
--
Pierre
Le 27/04/2021 à 18:46, David Gebler a écrit :
What's being proposed in the RFC is a functional change to the language
whereby attempting to extend a class designated as sealed to a
non-specified child results in a fatal error.It's not a functional change to the language, well, it is a new feature,
but it's actually not changing any paradigm in the language or the
engine. People will continue to write the same code, and people that
want to use it for some library or customer project internal purpose may
use it.
If you introduce it, people will use it and the people who use their code
will (no pun intended) inherit it. So the idea that no one is obligated to
use this pattern if they don't want to is at least de facto untrue.
Now that is not a benefit in itself, it is merely a description of the
proposed change. My question is who or what benefits from this change?
And
I look at this way:
The language engine doesn't benefit, since unlike some compiled
languages, there is no indication here it will result in improved opcode
sequences.The author of code doesn't benefit, to any extent greater than they
would by using an attribute or other metadata to indicate their
intentions,
because if someone else comes along, installs their library and creates
problems for themselves by extending some concrete type in a manner which
was not intended, the author's obligation to them is zero. If improperly
inheriting or otherwise misusing code creates a problem for a consumer,
it's the consumer's mess to sort out.I think that the debate "but if your seal your classes I won't be to
extend it" is overrated: it's not the language to chose whether or not
the library/software author can seal its classes or not, it's up the
library/software author to do its own choice.
Agree, which is precisely why I challenge there is any need for it to be
enforced in the language with a fatal error.
And in that regard, having
"sealed" classes in the language is actually bringing that possibility
to the library author, so it's more liberty.By design, by convention, in any of a company or an open source software
community, it can be legit to explicitly hard-seal stuff, for blocking
users when they are doing it wrong, if for example by attempting to
inherit they will break other existing piece code, for example. There's
many scenarios where it's legit to seal classes, and it can be a good
practice also (depend on your practices I guess).But if sealing means "you who want to change a behavior, you need to fix
other things deep in the code before being able to do that" then it's
good (and it's one of the use cases of sealing).So each time someone use the "but we won't be able to legitimately
extend your class" he's basically saying that people that do put very
thorough and strict conventions in their own code are wrong by nature
and don't understand what is good. It's almost an insult - please don't
misunderstand what I say,
I entirely understand and appreciate your view here, but it's no more
insulting than to say to your users "You are wrong by nature; I know your
use-case better than you do (without even seeing it or having knowledge of
it) and there is no legitimate reason for you to ever extend my class"
I don't think at any moment that any of the
people here really meant that, but it it's fundamentally what this
argument does: it says that some conventions are by nature bad and the
language should enforce people not to write code like this.
I think the language should not enforce any practice, convention or code
style, it must remain neutral considering people's whereabouts,
workflows or practices. If one's want to seal his or her class, let him
or her do it.
Some conventions may or may not be bad by nature but that's not the
argument I'm making, at all. The language enforcing one person's
preferences over another's is exactly what you are advocating and I am
cautioning against.
I think this point/argument should be banned from this discussion.
Given the behaviour we're talking about is literally the change proposed in
the RFC, I find this a very strange take.
Sealed classes are good under some conditions, maybe bad under others,
that's not the point, it's not about discussing each developer's,
community's or company's own conventions, but about is it OK technically
to add this feature to the language, and will it be or not a maintenance
burden, and finally will it actually break millions of lines of existing
code.I'm inclined to say yes, it is good to add this feature, and I'm
skeptical about the fact that it would break existing code (except for
people that named their classes "Permits"), there's many use cases I
wish I had it in the past. I can live with it, but sometime, it could be
the right tool.Regards,
--
Pierre
I think the conversation on final classes being "bad" has gone on long
enough. You don't like final, and you don't want to see more features
like it being added, such as sealed. Point taken. Now please stop
dominating the discussion, thank you.
On Tue, Apr 27, 2021 at 6:56 PM Levi Morrison levi.morrison@datadoghq.com
wrote:
I think the conversation on final classes being "bad" has gone on long
enough. You don't like final, and you don't want to see more features
like it being added, such as sealed. Point taken. Now please stop
dominating the discussion, thank you.
In a thread of around 50 messages, I have posted six of them, half of which
are addressing direct replies to the other half. I have also not treated
anyone rudely, disrespectfully, or gone off-topic. That is not dominating,
it is healthy discussion which should be encouraged. If you don't want to
read my contributions, I'm sure you can filter them.
Nonetheless, you may be pleased to know as far as I'm concerned, I have
fully expressed my view and reasoning on this RFC and have nothing further
I wish to add to the conversation.
I think the conversation on final classes being "bad" has gone on long
enough. You don't like final, and you don't want to see more features
like it being added, such as sealed. Point taken. Now please stop
dominating the discussion, thank you.In a thread of around 50 messages, I have posted six of them, half of which are addressing direct replies to the other half. I have also not treated anyone rudely, disrespectfully, or gone off-topic. That is not dominating, it is healthy discussion which should be encouraged. If you don't want to read my contributions, I'm sure you can filter them.
Nonetheless, you may be pleased to know as far as I'm concerned, I have fully expressed my view and reasoning on this RFC and have nothing further I wish to add to the conversation.
David,
Sorry, I did not mean you specifically, I mean the collective you as
in "you all." Sorry for the ambiguity.
On Tue, Apr 27, 2021 at 1:56 PM Levi Morrison via internals <
internals@lists.php.net> wrote:
I think the conversation on final classes being "bad" has gone on long
enough. You don't like final, and you don't want to see more features
like it being added, such as sealed. Point taken. Now please stop
dominating the discussion, thank you.
I think the legitimacy of final/sealed classes goes to the heart of this
RFC. As long as people are going to discuss it and bring up counter points,
then I think asking someone to stop defending their view is a bit out of
line.
That being said, David has never said he is against developers being able
to annotate their classes as being final or sealed. He is just against the
engine enforcing such requirements. On this I agree. I understand that
other languages support this concept - and frankly, I don't care. The
flexibility that PHP offers has always been one of its greatest strengths
and this just further erodes that.
--
To unsubscribe, visit: https://www.php.net/unsub.php
--
Chase Peeler
chasepeeler@gmail.com
2021-04-27 20:17 GMT+02:00, Chase Peeler chasepeeler@gmail.com:
On Tue, Apr 27, 2021 at 1:56 PM Levi Morrison via internals <
internals@lists.php.net> wrote:I think the conversation on final classes being "bad" has gone on long
enough. You don't like final, and you don't want to see more features
like it being added, such as sealed. Point taken. Now please stop
dominating the discussion, thank you.I think the legitimacy of final/sealed classes goes to the heart of this
RFC. As long as people are going to discuss it and bring up counter points,
then I think asking someone to stop defending their view is a bit out of
line.That being said, David has never said he is against developers being able
to annotate their classes as being final or sealed. He is just against the
engine enforcing such requirements. On this I agree. I understand that
other languages support this concept - and frankly, I don't care. The
flexibility that PHP offers has always been one of its greatest strengths
and this just further erodes that.--
To unsubscribe, visit: https://www.php.net/unsub.php
--
Chase Peeler
chasepeeler@gmail.com
Sometimes it's helpful to apply a risk perspective the shed some light
under hidden assumptions of different arguments. For example, what's
the probability and impact of an event that would limit a coder when a
library is using a sealed class? And the other way around, what's the
probability and impact of an event that would decrease code quality
when a sealed class is not used (like being able to subclass
Maybe/Option even when it "shouldn't" be possible)?
If the probability of such an event is high, but the impact to overall
code quality is low, the risk is also considered low. (Example: Just
create your own Maybe class. Of course harder with more elaborate
classes, you don't want to copy-paste an entire library. And the other
way, extending Maybe is a very local thing to do and doesn't hurt the
library itself.)
When both probability and impact are uncertain, it will make it harder
to create consensus, and will make arguments more emotional or
heuristic. When risk is low and the benefit high (and clear),
consensus is easy.
Olle
On Tue, Apr 27, 2021 at 2:57 PM Olle Härstedt olleharstedt@gmail.com
wrote:
2021-04-27 20:17 GMT+02:00, Chase Peeler chasepeeler@gmail.com:
On Tue, Apr 27, 2021 at 1:56 PM Levi Morrison via internals <
internals@lists.php.net> wrote:I think the conversation on final classes being "bad" has gone on long
enough. You don't like final, and you don't want to see more features
like it being added, such as sealed. Point taken. Now please stop
dominating the discussion, thank you.I think the legitimacy of final/sealed classes goes to the heart of this
RFC. As long as people are going to discuss it and bring up counter
points,
then I think asking someone to stop defending their view is a bit out of
line.That being said, David has never said he is against developers being able
to annotate their classes as being final or sealed. He is just against
the
engine enforcing such requirements. On this I agree. I understand that
other languages support this concept - and frankly, I don't care. The
flexibility that PHP offers has always been one of its greatest strengths
and this just further erodes that.--
To unsubscribe, visit: https://www.php.net/unsub.php
--
Chase Peeler
chasepeeler@gmail.comSometimes it's helpful to apply a risk perspective the shed some light
under hidden assumptions of different arguments. For example, what's
the probability and impact of an event that would limit a coder when a
library is using a sealed class? And the other way around, what's the
probability and impact of an event that would decrease code quality
when a sealed class is not used (like being able to subclass
Maybe/Option even when it "shouldn't" be possible)?
As someone mentioned above, maybe they want to just add some logging
capabilities to maybe.
class MyMaybe extends Maybe {
protected $logger;
public function setLogger($logger){ $this->logger = $logger; }
public function value(){
if(null !== $this->logger){ $this->logger->log("getting value"); }
return parent::value();
}
}
If the probability of such an event is high, but the impact to overall
code quality is low, the risk is also considered low. (Example: Just
create your own Maybe class. Of course harder with more elaborate
classes, you don't want to copy-paste an entire library. And the other
way, extending Maybe is a very local thing to do and doesn't hurt the
library itself.)
Copy/pasting Maybe into my own MyMaybe class isn't going to be a valid
option if I encounter something like
function(Maybe $maybe){...}
If I can subclass Maybe, then it will work. If MyMaybe is a totally
different class, though, it won't.
When both probability and impact are uncertain, it will make it harder
to create consensus, and will make arguments more emotional or
heuristic. When risk is low and the benefit high (and clear),
consensus is easy.Olle
--
Chase Peeler
chasepeeler@gmail.com
As someone mentioned above, maybe they want to just add some logging
capabilities to maybe.
class MyMaybe extends Maybe {
protected $logger;
public function setLogger($logger){ $this->logger = $logger; }
public function value(){
if(null !== $this->logger){ $this->logger->log("getting value"); }
return parent::value();
}
}
That is one argument. But maybe that argument points to the need for a different feature, one that could be considered for PHP instead of limiting the freedom of the library/framework author to decide how their code will be used?
We could add an Events feature that would allow users of a class to run a closure before or after every time a class' methods are run? People often suggest that attributes can be used to implement such features but then attributes can only be added by the author of the class, not injected by users of the class.
Imagine if something like the following existed (I just spitballed the syntax as a straw man suggestion; I doubt an actual events feature would look much like this after requirements were fleshed out):
/*
- Attach an event to run after every time the Maybe class was instantiated
*/
\PHP\Events::after( [ Maybe::class, "__construct" ], func($context){
// Attach a property to the instance that would only be visible inside of events
$context->logger = MyLogger::get();
})
/*
- Attach an event to run before every time the value() method was called an a Maybe instance
*/
\PHP\Events::before( [ Maybe::class, "value" ], func($context){
// Calllog()
on the attached $logger property
$context->logger->log("getting value");
})
/*
- Your logger class
*/
class MyLogger {
private static $logger;
public static function set($logger){
self::$logger = $logger;
}
public static function get(){
if (!isset(self::$logger)) {
self::$logger = new MyLogger();
}
return $logger;
}
public static function log($msg){
printf( "$1\n", $msg );
}
}
/*
- Example usage code
*/
$m = new Maybe(1)
echo $m->value(); // echos "getting value\n1"
In my view this would be more consistent with the open-closed principle of S.O.L.I.D. because it would allow a developer to augment classes in libraries and frameworks that hardcode instantiate — do not just dependency injection — something you cannot do with mere subclassing.
I know this suggestion is off-topic for this thread, but I introduce to point out at least some of the objections to sealed
could addressed better by new language features.
SO IF people are interested in discussing this specific language feature more, PLEASE break this off into a different thread.
-Mike
P.S. Also, I admit I did what I hate when other people do it; I focused on your example that you used to illustrate a general principle and I addressed that example only, not your general concern. But I only did so because other features would better address that specific example.
Do you have other examples that would illustrate why you might want to extend a Maybe class that could not be handled with events?
Sometimes it's helpful to apply a risk perspective the shed some light
under hidden assumptions of different arguments. For example, what's
the probability and impact of an event that would limit a coder when a
library is using a sealed class? And the other way around, what's the
probability and impact of an event that would decrease code quality
when a sealed class is not used (like being able to subclass
Maybe/Option even when it "shouldn't" be possible)?As someone mentioned above, maybe they want to just add some logging
capabilities to maybe.
class MyMaybe extends Maybe {
protected $logger;
public function setLogger($logger){ $this->logger = $logger; }
public function value(){
if(null !== $this->logger){ $this->logger->log("getting value"); }
return parent::value();
}
}
That's subtly different than what is being discussed here. Consider:
class Maybe { ... }
class Some extends Maybe { ... }
class None extends Maybe { ... }
And now assume we have pattern matching in the match expression (just to make the following example simpler; it could also be done with instanceof directives just as well but it's more verbose):
function operateOnValue(Maybe $m) {
$val = match($m) is (
Some($v) => $v,
None => $some_default,
};
...
}
This code works on Maybe as defined, with two branches, Some and None.
If you extend Some with your own custom Some, it still works. This code is fine. Such extension could be prevented by making Some final, but you can already do that today.
If you want to add another subclass of Maybe, called Perhaps... that function will now break, because the nominal contract of Maybe (that it has only two variants) has been broken. As soon as you pass a Perhaps to operateOnValue(), the function will fail with an exception. And there is no way to prevent that today.
The two options to prevent such errors are:
- Sealed classes.
- Extend Enums into ADTs.
They're different in a few key ways, but both address the same problem space and would render operateOnValue() safer against errors, because its developer can know, by definition, that it can handle all possible variants of Maybe. (That is, it's a total function over Maybe.)
A comment (by whatever syntax) saying "pretty please don't extend Maybe" is worth the executable code it generates (which is to say, None).
--Larry Garfield
On Tue, Apr 27, 2021 at 11:23 PM Larry Garfield larry@garfieldtech.com
wrote:
The two options to prevent such errors are:
- Sealed classes.
- Extend Enums into ADTs.
Unless I've misunderstood your example, there is a third option which quite
possibly prevents the error in a nicer, easier to reason about, more
flexible pattern.
class Perhaps extends Maybe implements OperatesOnTheValueInterface { ... }
On Tue, Apr 27, 2021 at 11:23 PM Larry Garfield larry@garfieldtech.com
wrote:The two options to prevent such errors are:
- Sealed classes.
- Extend Enums into ADTs.
Unless I've misunderstood your example, there is a third option which quite
possibly prevents the error in a nicer, easier to reason about, more
flexible pattern.class Perhaps extends Maybe implements OperatesOnTheValueInterface { ... }
If we were dealing with a service object in classic OOP (viz, OOP based on classes), then yes, turning the function into a method and using polymorphism would be the correct answer, rather than RTTI.
However! Classic OOP design patterns are not all that PHP supports, and that's a good thing. The "class" construct, for better or worse, is the syntax for logic objects, value objects, data objects, and control flow objects (such as Maybe, Either, etc.), plus assorted other patterns that are not part of the classic OOP canon. But they're still good and useful patterns, and often a better model than classic OOP "polymorph all the things" approaches.
If we were designing PHP from scratch today, I'd argue for having separate language constructs for funcy-syntax-closures (which is what service objects are), product types (structs, value objects, all the same thing), and control flow types. Many newer languages do differentiate those better. That's not where we are, though, so we're stuck with class being the uber-syntax for anything even slightly interesting from a type perspective. So be it, but it does lead to ample confusion about which use case you're talking about, especially when not everyone is familiar with all of the different, distinct use cases.
See also: This thread. :-)
Sealed classes are... not really useful at all for service object use cases. They are useful for product type and control flow type use cases.
--Larry Garfield
On Tue, Apr 27, 2021 at 11:23 PM Larry Garfield larry@garfieldtech.com
wrote:The two options to prevent such errors are:
- Sealed classes.
- Extend Enums into ADTs.
Unless I've misunderstood your example, there is a third option which quite
possibly prevents the error in a nicer, easier to reason about, more
flexible pattern.class Perhaps extends Maybe implements OperatesOnTheValueInterface { ... }
If we were dealing with a service object in classic OOP (viz, OOP based on classes), then yes, turning the function into a method and using polymorphism would be the correct answer, rather than RTTI.
However! Classic OOP design patterns are not all that PHP supports, and that's a good thing. The "class" construct, for better or worse, is the syntax for logic objects, value objects, data objects, and control flow objects (such as Maybe, Either, etc.), plus assorted other patterns that are not part of the classic OOP canon. But they're still good and useful patterns, and often a better model than classic OOP "polymorph all the things" approaches.
If we were designing PHP from scratch today, I'd argue for having separate language constructs for funcy-syntax-closures (which is what service objects are), product types (structs, value objects, all the same thing), and control flow types. Many newer languages do differentiate those better. That's not where we are, though, so we're stuck with class being the uber-syntax for anything even slightly interesting from a type perspective.
But, are we really, stuck?
You successfully introduced enumerations. Why not also data types and control flow types?
Just asking for a "friend."
So be it, but it does lead to ample confusion about which use case you're talking about, especially when not everyone is familiar with all of the different, distinct use cases.
See also: This thread. :-)
Sealed classes are... not really useful at all for service object use cases. They are useful for product type and control flow type use cases.
--Larry Garfield
--
To unsubscribe, visit: https://www.php.net/unsub.php
-Mike
On Tue, Apr 27, 2021 at 7:00 PM Larry Garfield larry@garfieldtech.com
wrote:
On Tue, Apr 27, 2021 at 11:23 PM Larry Garfield larry@garfieldtech.com
wrote:The two options to prevent such errors are:
- Sealed classes.
- Extend Enums into ADTs.
Unless I've misunderstood your example, there is a third option which
quite
possibly prevents the error in a nicer, easier to reason about, more
flexible pattern.class Perhaps extends Maybe implements OperatesOnTheValueInterface { ...
}If we were dealing with a service object in classic OOP (viz, OOP based on
classes), then yes, turning the function into a method and using
polymorphism would be the correct answer, rather than RTTI.However! Classic OOP design patterns are not all that PHP supports, and
that's a good thing. The "class" construct, for better or worse, is the
syntax for logic objects, value objects, data objects, and control flow
objects (such as Maybe, Either, etc.), plus assorted other patterns that
are not part of the classic OOP canon. But they're still good and useful
patterns, and often a better model than classic OOP "polymorph all the
things" approaches.If we were designing PHP from scratch today, I'd argue for having separate
language constructs for funcy-syntax-closures (which is what service
objects are), product types (structs, value objects, all the same thing),
and control flow types. Many newer languages do differentiate those
better. That's not where we are, though, so we're stuck with class being
the uber-syntax for anything even slightly interesting from a type
perspective. So be it, but it does lead to ample confusion about which use
case you're talking about, especially when not everyone is familiar with
all of the different, distinct use cases.See also: This thread. :-)
Sealed classes are... not really useful at all for service object use
cases. They are useful for product type and control flow type use cases.
And I think that is where our approaches differ. You feel that since sealed
classes are useful for at least one of the ways classes can be used in PHP,
then we should make them available even if that means they end up getting
used with the ways that they don't make sense. My feeling is that we
shouldn't introduce something that would cause restrictions where they
don't make sense just so we can have them where they do.
--Larry Garfield
--
To unsubscribe, visit: https://www.php.net/unsub.php
--
Chase Peeler
chasepeeler@gmail.com
Okay, I promise I'll make this my last word on the thread, for the sake of
my own sanity and at this point, I'm sure that of others - first, I really
appreciate your explanation there for why you consider this RFC a good
thing, so thank you for that. Personally, my objections still stand for in
essence the same reasons as before;
-
In respect of data modelling, you may think you've understood a model
such that its sum is closed, this may even be true for your use case (and
yes, agree both that this is useful and some other languages have better
ways of expressing it) but sometimes, it is only true until it isn't. E.g.
you have said colours are red, green and blue. This is fine until someone
else needs to reason about purple and can't because your data model says
there is no purple. -
A (non-functional) attribute [or other syntax/convention] for sealed
does not / should not be interpreted as saying "pretty please, don't extend
me", it says "I don't endorse/support this, but if I haven't accounted for
your use case and you're confident you understand your problem, hey, don't
let me stand in your way" - other interpreted languages (Python comes to
mind) use this approach very effectively and their ecosystems are not
automatically riddled with bad models as a result. Indeed PHP is in some
respects unusually strict for an interpreted language (though I am not
saying that is necessarily a bad thing). -
That there are valid use cases for a language construct does not mean
people will only use it for those cases - I'd go as far as to say they
definitely won't. Annotation in some form is safe in all use cases, fatal
errors aren't. So I stand by the real, primary benefit of these constructs
- delineation of intent boundaries - is achieved without creating a
construct which introduces a new fatal error condition.
Anyway, yeah - I've said my piece(s) and I'm not a voting member so what I
think doesn't matter to that end. I hope my input on internals discussions
is not entirely moot, though; just like everyone else I take part because
I'm very fond of PHP and have an interest in its future. Appreciate those
who are willing to hear me out.
On Wed, Apr 28, 2021 at 12:00 AM Larry Garfield larry@garfieldtech.com
wrote:
On Tue, Apr 27, 2021 at 11:23 PM Larry Garfield larry@garfieldtech.com
wrote:The two options to prevent such errors are:
- Sealed classes.
- Extend Enums into ADTs.
Unless I've misunderstood your example, there is a third option which
quite
possibly prevents the error in a nicer, easier to reason about, more
flexible pattern.class Perhaps extends Maybe implements OperatesOnTheValueInterface { ...
}If we were dealing with a service object in classic OOP (viz, OOP based on
classes), then yes, turning the function into a method and using
polymorphism would be the correct answer, rather than RTTI.However! Classic OOP design patterns are not all that PHP supports, and
that's a good thing. The "class" construct, for better or worse, is the
syntax for logic objects, value objects, data objects, and control flow
objects (such as Maybe, Either, etc.), plus assorted other patterns that
are not part of the classic OOP canon. But they're still good and useful
patterns, and often a better model than classic OOP "polymorph all the
things" approaches.If we were designing PHP from scratch today, I'd argue for having separate
language constructs for funcy-syntax-closures (which is what service
objects are), product types (structs, value objects, all the same thing),
and control flow types. Many newer languages do differentiate those
better. That's not where we are, though, so we're stuck with class being
the uber-syntax for anything even slightly interesting from a type
perspective. So be it, but it does lead to ample confusion about which use
case you're talking about, especially when not everyone is familiar with
all of the different, distinct use cases.See also: This thread. :-)
Sealed classes are... not really useful at all for service object use
cases. They are useful for product type and control flow type use cases.--Larry Garfield
--
To unsubscribe, visit: https://www.php.net/unsub.php
Anyway, yeah - I've said my piece(s) and I'm not a voting member so what I
think doesn't matter to that end. I hope my input on internals discussions
is not entirely moot, though; just like everyone else I take part because
I'm very fond of PHP and have an interest in its future. Appreciate those
who are willing to hear me out.
I wanted to take a moment to say that your voice is important, even if
you’re not a voting member. You’re a user of the language, and that
matters to those who vote. There may be fundamental disagreements here
on the list about the direction of the language, but taking the time to
weigh-in improves these RFCs for the better.
As with many things in life, we have to make compromises to work
together, but please don’t let that discourage you (or anyone else, for
that matter) from voicing concerns over how a proposed feature might
change the way you use the language. Someone else might not have
considered your perspective, and it might bring changes to the RFC or
change someone’s mind about how they’ll vote.
So, keep it up!
Cheers,
Ben
Am 28.04.2021 um 01:00 schrieb Larry Garfield larry@garfieldtech.com:
However! Classic OOP design patterns are not all that PHP supports, and that's a good thing. The "class" construct, for better or worse, is the syntax for logic objects, value objects, data objects, and control flow objects (such as Maybe, Either, etc.), plus assorted other patterns that are not part of the classic OOP canon. But they're still good and useful patterns, and often a better model than classic OOP "polymorph all the things" approaches.
If we were designing PHP from scratch today, I'd argue for having separate language constructs for funcy-syntax-closures (which is what service objects are), product types (structs, value objects, all the same thing), and control flow types. Many newer languages do differentiate those better. That's not where we are, though, so we're stuck with class being the uber-syntax for anything even slightly interesting from a type perspective. So be it, but it does lead to ample confusion about which use case you're talking about, especially when not everyone is familiar with all of the different, distinct use cases.
See also: This thread. :-)
Sealed classes are... not really useful at all for service object use cases. They are useful for product type and control flow type use cases.
... and ...
Am 28.04.2021 um 09:39 schrieb Pierre pierre-php@processus.org:
Yeah, final is something, I originally come from the Java world 20 years back, and it never hit me as something terrible in PHP, on the contrary, I use it "per default" personally to promote composition over inheritance, but when I do that I mostly write interface and composition based APIs which much more flexibility than an open to extension design (TL;DR: I always give the user an escape route other than inheritance, and document it).
Those are good points you are bringing up.
You're basically saying that classic OOP has outlived itself in many cases - something I agree with - and we need the ability to use other patterns.
With that in mind I'm not sure if we really want to tack all this functionality on to "class".
I think it would be better and less confusing to most developers if we were to bite the bullet and try to do it right.
Especially since my impression is that we only need to tack it on to "class" if there is urgency and personally I'm not feeling it.
But then again YMMV.
You convinced me from a -1 to considering abstaining from the vote ;-)
- Chris
Am 28.04.2021 um 01:00 schrieb Larry Garfield larry@garfieldtech.com:
However! Classic OOP design patterns are not all that PHP supports, and that's a good thing. The "class" construct, for better or worse, is the syntax for logic objects, value objects, data objects, and control flow objects (such as Maybe, Either, etc.), plus assorted other patterns that are not part of the classic OOP canon. But they're still good and useful patterns, and often a better model than classic OOP "polymorph all the things" approaches.
If we were designing PHP from scratch today, I'd argue for having separate language constructs for funcy-syntax-closures (which is what service objects are), product types (structs, value objects, all the same thing), and control flow types. Many newer languages do differentiate those better. That's not where we are, though, so we're stuck with class being the uber-syntax for anything even slightly interesting from a type perspective. So be it, but it does lead to ample confusion about which use case you're talking about, especially when not everyone is familiar with all of the different, distinct use cases.
See also: This thread. :-)
Sealed classes are... not really useful at all for service object use cases. They are useful for product type and control flow type use cases.
... and ...
Am 28.04.2021 um 09:39 schrieb Pierre pierre-php@processus.org:
Yeah, final is something, I originally come from the Java world 20 years back, and it never hit me as something terrible in PHP, on the contrary, I use it "per default" personally to promote composition over inheritance, but when I do that I mostly write interface and composition based APIs which much more flexibility than an open to extension design (TL;DR: I always give the user an escape route other than inheritance, and document it).
Those are good points you are bringing up.
You're basically saying that classic OOP has outlived itself in many
cases - something I agree with - and we need the ability to use other
patterns.With that in mind I'm not sure if we really want to tack all this
functionality on to "class".
I think it would be better and less confusing to most developers if we
were to bite the bullet and try to do it right.
Especially since my impression is that we only need to tack it on to
"class" if there is urgency and personally I'm not feeling it.
But then again YMMV.You convinced me from a -1 to considering abstaining from the vote ;-)
Ha! Progress. :-P
As I've indicated before, there are two possible paths to an essentially similar end goal, both of which have RFCs written but no code written: Sealed classes and the ADTs proposal. Sealed classes is built on, well, classes. ADTs are build on enums, which in PHP are themselves built on classes. In practice, some languages do one, some do the other, and I think one or two may do both.
For the use cases I can envision, I think either would work. Sealed classes would be a bit more flexible, while Enum-based ADTs would be more compact and convenient in the typical case. There's one or two use cases I'd like to be able to implement where ADTs would probably not be sufficient (state machines), but then Sealed classes would be annoyingly verbose in those cases, so...
I'm still mostly open to either approach, and not convinced that we need both (though I could be convinced, potentially). In practice, I think I'm good with whichever one someone is able to get implemented first, which is always the hard part of any proposal.
(Someone else, I forget who, suggested type aliasing as another solution, where you'd just have multiple classes and, in effect, type alias a union of those classes to a new name and tell people to use that. I like type aliases, but I'm not convinced that's a viable solution in this case. It's very round-about.)
--Larry Garfield
Le 27/04/2021 à 19:51, David Gebler a écrit :
I don't think at any moment that any of the
people here really meant that, but it it's fundamentally what this
argument does: it says that some conventions are by nature bad and the
language should enforce people not to write code like this.
I think the language should not enforce any practice, convention or code
The sealed feature does not enforce anything. It brings a new tool in
already quite furnished toolbox. It doesn't force you to use it (and
besides I think it's a good tool, but as I said, I can live without).
Some conventions may or may not be bad by nature but that's not the
argument I'm making, at all. The language enforcing one person's
preferences over another's is exactly what you are advocating and I am
cautioning against.
It's not enforcing one person's preference over another's, people should
be able to seal whatever they want to seal in their own code. I repeat,
their code, not yours. If you don't like another person's code because
you can't override a class, then fork it or use another library. And
beside, I doubt that people will use it so widely that it'll prevent you
from doing anything good. Or if they do so, they are probably not
writing a so good piece of software (beware, subjectivity in the last
sentence).
By preventing so hard such feature to be included on this sole argument,
you are actually dictating your own preference, not the other way around.
I think that sealed class usage remains an edge case, and it's basically
meant to avoid users to shooting themselves in the foot and open the
discussion when they think it's not a good choice.
I think this point/argument should be banned from this discussion.
Given the behaviour we're talking about is literally the change proposed in
the RFC, I find this a very strange take.
I don't think it is, it's not how I understood it at the very least.
Please don't take anything I said as being disrespectful (I'm not a
native english speaker and sometime I do not measure my own words), on
the contrary it's an interesting discussion.
Regards,
--
Pierre
Am 27.04.2021 um 19:19 schrieb Pierre pierre-php@processus.org:
I think that the debate "but if your seal your classes I won't be to extend it" is overrated: it's not the language to chose whether or not the library/software author can seal its classes or not, it's up the library/software author to do its own choice. And in that regard, having "sealed" classes in the language is actually bringing that possibility to the library author, so it's more liberty.
The same - "it is more liberty" - could be said about operator overloading, multiple inheritance and many other language features which lead to people using them in ways / places I consider harmful. So I'd rather not have them in my language of choice.
I'm not saying the feature is completely useless in the right hands but I think it will be misunderstood and promotes bad habits. Think people abusing exceptions for flow control.
But then again maybe I'm just traumatized by people putting final all over the place in Java back in the days. I'm sure people in 2021 know better than that /s ;-)
- Chris
Le 28/04/2021 à 00:25, Christian Schneider a écrit :
The same - "it is more liberty" - could be said about operator overloading, multiple inheritance and many other language features which lead to people using them in ways / places I consider harmful. So I'd rather not have them in my language of choice.
Yes, I get your point.
Anyway, operator overloading and multiple inheritance raise much more
complex technical details, and much more specification to be written,
and induce much more magic in the air !
Sealing a class doesn't induce any change in behavior, doesn't induce
any functional spec, doesn't hide any runtime flow behind magic, doesn't
forces to redact and implement some kind of resolution order mechanism
or hidden magic, it's just a finer variant of the already existing final
keyword.
I'm not saying the feature is completely useless in the right hands but I think it will be misunderstood and promotes bad habits. Think people abusing exceptions for flow control.
Any powerful tool can be misused, I mean this argument is real for all
of: exceptions, switch statements, final methods, traits, basic
inheritance, magic methods, bit flags, type coercion, etc, etc etc,... I
think that the sealed classes, by the fact they reduce usability of
classes themselves is not something that gets promoted, but rather
something that will be used in edge cases where it makes sense.
Of course, people will always end up, at some point, using it the wrong
way, but hey, we're all doing open source, community feedback and review
is the best of all ways to avoid that major libraries or software used
by many will end up polluting their code with such bad code :)
But then again maybe I'm just traumatized by people putting final all over the place in Java back in the days. I'm sure people in 2021 know better than that /s ;-)
Yeah, final is something, I originally come from the Java world 20 years
back, and it never hit me as something terrible in PHP, on the contrary,
I use it "per default" personally to promote composition over
inheritance, but when I do that I mostly write interface and composition
based APIs which much more flexibility than an open to extension design
(TL;DR: I always give the user an escape route other than inheritance,
and document it).
Even in my daily work working for client proprietary projects, I always
write final per default everywhere, so that we spot those changes in
peer review and have the reflex to ask "why did you extend ? wasn't it
possible to decorate, write an interface, or fix the bug ?". In that
regard, for many cases, sealed classes may be something to us, because
it could fit in our production workflow, not everywhere, (I'd reassure
you, I'm not going to write "sealed" everywhere) nevertheless it has a
real added value and meaning when writing business critical code path
that need extra testing and stability in the long term.
Regards
--
Pierre
Am 24.04.2021 um 21:51 schrieb Marco Pivetta ocramius@gmail.com:
2021-04-24 17:59 GMT+02:00, Saif Eddin Gmati azjezz@void.tn:
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.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?
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
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:
On Sat, Apr 24, 2021, 21:44 Olle Härstedt <olleharstedt@gmail.com
mailto:olleharstedt@gmail.com> wrote:2021-04-24 17:59 GMT+02:00, Saif Eddin Gmati azjezz@void.tn:
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.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?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
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:
- 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.
- 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/
Still, all these problems are solved to the same degree if you add a
#[Sealed] attribute to a class which has no functional impact. You have
sufficiently indicated to any user that extending this class is not a
designed feature and may cause backwards-incompatible breaks with future
releases - in a way that both a programmer and IDE can reason about, which
in PHP's context is what matters. Attributes arguably even have a greater
qualitative advantage that they can be applied right down as far as
individual method parameters.
In Java the idea of final and sealed classes makes more sense, since we
actually to some extent need the compiler to be able to reason about these
types and can gain optimization and code generation benefits from its being
able to do so. PHP's concept of typing and implementation of type checking
as an interpreted language is completely different.
I wonder, if final and sealed as language constructs really offer the
guarantees about intent and safety their advocates say they do, why are
they not the default? Why can no one point me to a language where I have to
write something like
extendable class Foo permits all { .... }
(and there are people who would be in favour of making inheritability this
explicit, but I'm not one of them)
It's one thing as an author of code to say "I only intended and support
this finite set of use-cases", it's quite another to say "and you should be
impeded from proceeding with any legitimate use-case I didn't imagine or
foresee"
In practice, the only thing I've ever seen this achieve is to create
difficulties, while the claimed benefits can be adequately (and better)
achieved through existing patterns like annotations, interfaces and DI.
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:
- 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 asFiveStarRating
which
could only contain 1, 2, 3, 4 or 5, or types that identify a signature, for
example asConvertToString
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.
- 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/
Stitching together 2 replies to minimize thread noise...
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
That's actually a pretty solid argument against attributes here, honestly. Consider me convinced, and now in favor of "final class Foo permits Bar, Baz". :-)
Still, all these problems are solved to the same degree if you add a
#[Sealed] attribute to a class which has no functional impact. You have
sufficiently indicated to any user that extending this class is not a
designed feature and may cause backwards-incompatible breaks with future
releases - in a way that both a programmer and IDE can reason about, which
in PHP's context is what matters. Attributes arguably even have a greater
qualitative advantage that they can be applied right down as far as
individual method parameters.In Java the idea of final and sealed classes makes more sense, since we
actually to some extent need the compiler to be able to reason about these
types and can gain optimization and code generation benefits from its being
able to do so. PHP's concept of typing and implementation of type checking
as an interpreted language is completely different.I wonder, if final and sealed as language constructs really offer the
guarantees about intent and safety their advocates say they do, why are
they not the default? Why can no one point me to a language where I have to
write something likeextendable class Foo permits all { .... }
(and there are people who would be in favour of making inheritability this
explicit, but I'm not one of them)It's one thing as an author of code to say "I only intended and support
this finite set of use-cases", it's quite another to say "and you should be
impeded from proceeding with any legitimate use-case I didn't imagine or
foresee"In practice, the only thing I've ever seen this achieve is to create
difficulties, while the claimed benefits can be adequately (and better)
achieved through existing patterns like annotations, interfaces and DI.
Part of the challenge here, i think, is that PHP, like most classic OOP languages, uses the same syntax for two different things. One is for product types (an int combined with a string together in a single struct), the other is bound values in closures (aka, methods in service objects). That's arguably by design in classic OOP, and arguably a design flow in classic OOP. :-)
I can think of no reason at all to make a service object's class sealed. For those, yes, you shouldn't forbid custom extension, and frankly I usually discourage the use of private variables, too, for the same reason.
For product types that are part of the domain, however, there are definitely use cases where you want a defined finite list of possible variants. The Distance and Maybe examples upthread, for instance. That's not always the case, but there are definitely cases for them.
--Larry Garfield
Le 25/04/2021 à 21:22, Larry Garfield a écrit :
Stitching together 2 replies to minimize thread noise...
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
That's actually a pretty solid argument against attributes here, honestly. Consider me convinced, and now in favor of "final class Foo permits Bar, Baz". :-)
Yes, even though I was the first mail suggesting it in the beginning,
this is a solid argument which actually do change my mind.
In the end, I like the class Foo permis Bar, Baz
syntax, with a single
keyword added.
--
Pierre
2021-04-26 9:22 GMT+02:00, Pierre pierre-php@processus.org:
Le 25/04/2021 à 21:22, Larry Garfield a écrit :
Stitching together 2 replies to minimize thread noise...
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
That's actually a pretty solid argument against attributes here, honestly.
Consider me convinced, and now in favor of "final class Foo permits Bar,
Baz". :-)Yes, even though I was the first mail suggesting it in the beginning,
this is a solid argument which actually do change my mind.In the end, I like the
class Foo permis Bar, Baz
syntax, with a single
keyword added.--
Pierre
--
To unsubscribe, visit: https://www.php.net/unsub.php
Is there actually a bug that this functionality can/could prevent? I
get that Maybe and Result types should be closed, but what are the
risk of software defects if someone abuses that fact (locally)?
Olle
Le 26/04/2021 à 09:35, Olle Härstedt a écrit :
2021-04-26 9:22 GMT+02:00, Pierre pierre-php@processus.org:
Le 25/04/2021 à 21:22, Larry Garfield a écrit :
Stitching together 2 replies to minimize thread noise...
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
That's actually a pretty solid argument against attributes here, honestly.
Consider me convinced, and now in favor of "final class Foo permits Bar,
Baz". :-)Yes, even though I was the first mail suggesting it in the beginning,
this is a solid argument which actually do change my mind.In the end, I like the
class Foo permis Bar, Baz
syntax, with a single
keyword added.--
Pierre
--
To unsubscribe, visit: https://www.php.net/unsub.php
Is there actually a bug that this functionality can/could prevent? I
get that Maybe and Result types should be closed, but what are the
risk of software defects if someone abuses that fact (locally)?
I don't know if you replied to the right mail, I should have specified I
was talking about using an attribute versus adding a new keyword to the
language.
--
Pierre
2021-04-26 9:37 GMT+02:00, Pierre pierre-php@processus.org:
Le 26/04/2021 à 09:35, Olle Härstedt a écrit :
2021-04-26 9:22 GMT+02:00, Pierre pierre-php@processus.org:
Le 25/04/2021 à 21:22, Larry Garfield a écrit :
Stitching together 2 replies to minimize thread noise...
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
That's actually a pretty solid argument against attributes here,
honestly.
Consider me convinced, and now in favor of "final class Foo permits
Bar,
Baz". :-)Yes, even though I was the first mail suggesting it in the beginning,
this is a solid argument which actually do change my mind.In the end, I like the
class Foo permis Bar, Baz
syntax, with a single
keyword added.--
Pierre
--
To unsubscribe, visit: https://www.php.net/unsub.php
Is there actually a bug that this functionality can/could prevent? I
get that Maybe and Result types should be closed, but what are the
risk of software defects if someone abuses that fact (locally)?I don't know if you replied to the right mail, I should have specified I
was talking about using an attribute versus adding a new keyword to the
language.
Sorry, I was replying to the thread in general, not your reply
specifically. ^^ Maybe I should have replied to the top mail instead,
sorry.
Olle
Still, all these problems are solved to the same degree if you add a
#[Sealed] attribute to a class which has no functional impact. You have
sufficiently indicated to any user that extending this class is not a
designed feature and may cause backwards-incompatible breaks with future
releases - in a way that both a programmer and IDE can reason about, which
in PHP's context is what matters. Attributes arguably even have a greater
qualitative advantage that they can be applied right down as far as
individual method parameters.
In my experience, if a developer needs access to a feature that is easily available via the use of a method that is merely documented as internal-use only, the developer will use it rather anyway than spend a lot more time trying to work around what that method makes easily available. Especially if that documented internal-use only is about not subclassing.
And if many other developers do that, the original developer will still have the same problem as if they didn't document it as internal-use only.
But I do acknowledge your experience might be different. FWIW.
In Java the idea of final and sealed classes makes more sense, since we
actually to some extent need the compiler to be able to reason about these
types and can gain optimization and code generation benefits from its being
able to do so. PHP's concept of typing and implementation of type checking
as an interpreted language is completely different.I wonder, if final and sealed as language constructs really offer the
guarantees about intent and safety their advocates say they do, why are
they not the default? Why can no one point me to a language where I have to
write something likeextendable class Foo permits all { .... }
(and there are people who would be in favour of making inheritability this
explicit, but I'm not one of them)
Well, I can't point you to a language that works that way because I don't know more than a handful of languages in depth — although there may be one — but I can point you to a language that does not even allow you to subclass: Go.
The Go designers wanted to get away from the fragile base class problem so they provided embedding instead:
https://medium.com/@simplyianm/why-gos-structs-are-superior-to-class-based-inheritance-b661ba897c67
I program more in Go now than in PHP, and I can confirm that its approach works brilliantly. Better IMO than what PHP currently offers in that respect.
It's one thing as an author of code to say "I only intended and support
this finite set of use-cases", it's quite another to say "and you should be
impeded from proceeding with any legitimate use-case I didn't imagine or
foresee"
I do respect that as a user of other developer's code you might view it that way.
But then I also can respect the library or framework developer who chooses to make their own job less difficult by (wanting to) mark a class to be sealed.
You claim "it's quite another thing to say" but I don't think a developer of library or framework code should be required to offer features to their users they do not want to offer. It is the developer's prerogative; if they want to be able to mark their classes as sealed they should be empowered to do so. IMHO, anyway.
In practice, the only thing I've ever seen this achieve is to create
difficulties, while the claimed benefits can be adequately (and better)
achieved through existing patterns like annotations, interfaces and DI.
I would argue that maybe the reason you have only seen it create difficulties is because of your own perspective and experiences that are, by definition, anecdotal and thus limited.
OTOH Fabien Potencier and/or Taylor Otwell might have a different experiences and thus a different perspective on what those benefits are (but I am admittedly just hypothesizing here.)
-Mike
Still, all these problems are solved to the same degree if you add a
#[Sealed] attribute to a class which has no functional impact. You have
sufficiently indicated to any user that extending this class is not a
designed feature and may cause backwards-incompatible breaks with future
releases - in a way that both a programmer and IDE can reason about,
which
in PHP's context is what matters. Attributes arguably even have a greater
qualitative advantage that they can be applied right down as far as
individual method parameters.In my experience, if a developer needs access to a feature that is easily
available via the use of a method that is merely documented as internal-use
only, the developer will use it rather anyway than spend a lot more time
trying to work around what that method makes easily available. Especially
if that documented internal-use only is about not subclassing.And if many other developers do that, the original developer will still
have the same problem as if they didn't document it as internal-use only.But I do acknowledge your experience might be different. FWIW.
You're answering a different point to the one I'm making, which is that in
contrast to a language like Java, where the compiler gets a benefit out of
reasoning about virtual vs non-virtual methods, the use of sealed as a
language construct in an interpreted language like PHP can do nothing which
isn't already achieved by annotation - the expression of intent.
In Java the idea of final and sealed classes makes more sense, since we
actually to some extent need the compiler to be able to reason about
these
types and can gain optimization and code generation benefits from its
being
able to do so. PHP's concept of typing and implementation of type
checking
as an interpreted language is completely different.I wonder, if final and sealed as language constructs really offer the
guarantees about intent and safety their advocates say they do, why are
they not the default? Why can no one point me to a language where I have
to
write something likeextendable class Foo permits all { .... }
(and there are people who would be in favour of making inheritability
this
explicit, but I'm not one of them)Well, I can't point you to a language that works that way because I don't
know more than a handful of languages in depth — although there may be one
— but I can point you to a language that does not even allow you to
subclass: Go.The Go designers wanted to get away from the fragile base class problem so
they provided embedding instead:https://medium.com/@simplyianm/why-gos-structs-are-superior-to-class-based-inheritance-b661ba897c67
I program more in Go now than in PHP, and I can confirm that its approach
works brilliantly. Better IMO than what PHP currently offers in that
respect.
Again, this doesn't seem relevant to the discussion in respect of PHP or
the RFC we're talking about. I don't have a Go background but I'm learning
it at the moment and I like it a lot. Nonetheless it's neither comparable
to PHP nor object oriented in a conventional sense so any direct comparison
here is probably not too useful.
It's one thing as an author of code to say "I only intended and support
this finite set of use-cases", it's quite another to say "and you should
be
impeded from proceeding with any legitimate use-case I didn't imagine or
foresee"I do respect that as a user of other developer's code you might view it
that way.But then I also can respect the library or framework developer who chooses
to make their own job less difficult by (wanting to) mark a class to be
sealed.You claim "it's quite another thing to say" but I don't think a developer
of library or framework code should be required to offer features to their
users they do not want to offer. It is the developer's prerogative; if
they want to be able to mark their classes as sealed they should be
empowered to do so. IMHO, anyway.
My argument is not that there aren't legitimate cases where you want to
indicate a class or interface as sealed, nor that developers should not be
empowered to make this indication. It is that in PHP, as an interpreted
language, there is little to no benefit in expressing this through a new
language construct than through existing constructs and design patterns.
In practice, the only thing I've ever seen this achieve is to create
difficulties, while the claimed benefits can be adequately (and better)
achieved through existing patterns like annotations, interfaces and DI.I would argue that maybe the reason you have only seen it create
difficulties is because of your own perspective and experiences that are,
by definition, anecdotal and thus limited.OTOH Fabien Potencier and/or Taylor Otwell might have a different
experiences and thus a different perspective on what those benefits are
(but I am admittedly just hypothesizing here.)
Allow me to re-phrase then, in a way which removes anecdotes from the
equation; what is the problem in PHP which would be solved by adding the
sealed keyword (or any equivalent) and can't be solved via attributes or
other features already part of the language? There's no runtime benefit and
an IDE can read attributes to tell you you're breaking someone else's
intentions and prevent you making a mistake in respect of that.
Part of the argument I see for these features in languages which have them
is that inheritance is dangerous. And it certainly can be, but it can also
be dangerous to put in new language features which can be misunderstood and
used inappropriately. I just think we need to carefully weigh up the
cost/benefit of new language features (particularly new syntax).
-Mike
My argument is not that there aren't legitimate cases where you want to
indicate a class or interface as sealed, nor that developers should not be
empowered to make this indication.
Right here is the core point. This is a subjective statement, and one with which I (and apparently many here) disagree.
Note that the exact same argument could be made for typing parameters. I can document via a docblock that I expect a given parameter to be a string, or a Request object, or whatever. "There is little to no benefit in expressing that through a new language construct rather than through existing constructs and design patterns."
Except there very much is a benefit, for making the intent of code clearer and for allowing the engine to syntactically make certain invalid states impossible. Adding more typing power to PHP has been a phenomenal win over the past 15 years precisely because it converts certain bugs and error conditions into fatal errors with no additional work required on the part of the developer. That is a good thing.
ADTs, sealed classes, enum, and other functionality in that space is a continuation of the same process: Allow developers to use the language syntax to make invalid states impossible to describe, and thus bugs from invalid state go away.
Documentation, in whatever form, simply can't do that.
It has nothing to do with "inheritance is dangerous." It has to do with accurately and completely describing the problem space.
Also, sure, you can fork any library and hack out the parts you don't like. That is true for sealed classes, for enums, for property types, for the entire language. It's also completely 100% irrelevant, precisely because you're modifying upstream code, so all bets are off anyway.
--Larry Garfield
I did not expect nor intend to go down the rabbit hole regarding this topic, so this will likely be my last reply on this thread.
Still, all these problems are solved to the same degree if you add a
#[Sealed] attribute to a class which has no functional impact. You have
sufficiently indicated to any user that extending this class is not a
designed feature and may cause backwards-incompatible breaks with future
releases - in a way that both a programmer and IDE can reason about,
which
in PHP's context is what matters. Attributes arguably even have a greater
qualitative advantage that they can be applied right down as far as
individual method parameters.In my experience, if a developer needs access to a feature that is easily
available via the use of a method that is merely documented as internal-use
only, the developer will use it rather anyway than spend a lot more time
trying to work around what that method makes easily available. Especially
if that documented internal-use only is about not subclassing.And if many other developers do that, the original developer will still
have the same problem as if they didn't document it as internal-use only.But I do acknowledge your experience might be different. FWIW.
You're answering a different point to the one I'm making, which is that in
contrast to a language like Java, where the compiler gets a benefit out of
reasoning about virtual vs non-virtual methods, the use of sealed as a
language construct in an interpreted language like PHP can do nothing which
isn't already achieved by annotation - the expression of intent.
Assuming the annotation is only advisory and the keyword is actionable then of course PHP can do something not already achieved by annotation; it can throw an error when it comes across a class that attempts to extend a sealed class that has not named it as an exception.
In Java the idea of final and sealed classes makes more sense, since we
actually to some extent need the compiler to be able to reason about
these
types and can gain optimization and code generation benefits from its
being
able to do so. PHP's concept of typing and implementation of type
checking
as an interpreted language is completely different.I wonder, if final and sealed as language constructs really offer the
guarantees about intent and safety their advocates say they do, why are
they not the default? Why can no one point me to a language where I have
to
write something likeextendable class Foo permits all { .... }
(and there are people who would be in favour of making inheritability
this
explicit, but I'm not one of them)Well, I can't point you to a language that works that way because I don't
know more than a handful of languages in depth — although there may be one
— but I can point you to a language that does not even allow you to
subclass: Go.The Go designers wanted to get away from the fragile base class problem so
they provided embedding instead:https://medium.com/@simplyianm/why-gos-structs-are-superior-to-class-based-inheritance-b661ba897c67
I program more in Go now than in PHP, and I can confirm that its approach
works brilliantly. Better IMO than what PHP currently offers in that
respect.Again, this doesn't seem relevant to the discussion in respect of PHP or
the RFC we're talking about. I don't have a Go background but I'm learning
it at the moment and I like it a lot. Nonetheless it's neither comparable
to PHP nor object oriented in a conventional sense so any direct comparison
here is probably not too useful.
If you are going to equivocate and add more limiting criteria to your claim post hoc then of course the example I provided won't be too useful.
It's one thing as an author of code to say "I only intended and support
this finite set of use-cases", it's quite another to say "and you should
be
impeded from proceeding with any legitimate use-case I didn't imagine or
foresee"I do respect that as a user of other developer's code you might view it
that way.But then I also can respect the library or framework developer who chooses
to make their own job less difficult by (wanting to) mark a class to be
sealed.You claim "it's quite another thing to say" but I don't think a developer
of library or framework code should be required to offer features to their
users they do not want to offer. It is the developer's prerogative; if
they want to be able to mark their classes as sealed they should be
empowered to do so. IMHO, anyway.My argument is not that there aren't legitimate cases where you want to
indicate a class or interface as sealed, nor that developers should not be
empowered to make this indication. It is that in PHP, as an interpreted
language, there is little to no benefit in expressing this through a new
language construct than through existing constructs and design patterns.
I'll just leave this as-is since Larry and Saif have already explained how there is in-fact benefit for sealed
in PHP.
In practice, the only thing I've ever seen this achieve is to create
difficulties, while the claimed benefits can be adequately (and better)
achieved through existing patterns like annotations, interfaces and DI.I would argue that maybe the reason you have only seen it create
difficulties is because of your own perspective and experiences that are,
by definition, anecdotal and thus limited.OTOH Fabien Potencier and/or Taylor Otwell might have a different
experiences and thus a different perspective on what those benefits are
(but I am admittedly just hypothesizing here.)Allow me to re-phrase then, in a way which removes anecdotes from the
equation; what is the problem in PHP which would be solved by adding the
sealed keyword (or any equivalent) and can't be solved via attributes or
other features already part of the language? There's no runtime benefit and
an IDE can read attributes to tell you you're breaking someone else's
intentions and prevent you making a mistake in respect of that.
As I said above, there is indeed a runtime benefit. PHP can throw an error and disallow extending sealed classes that do not have stated permission.
Part of the argument I see for these features in languages which have them
is that inheritance is dangerous.
I do not see it as "dangerous" though you are certainly free to view it that way.
Having sealed
would allow developer to significantly reduce the number of scenarios where they would need to bump major version numbers because of breaking changes since, in reality, any newly added method to an existing extensible class is potentially a breaking change.
Not having sealed
is not so much "dangerous" as it increases the likelihood that a new release should be classified as a breaking change and thus requires everyone who uses the release to consider whether or not the new release will break their code. Even if a developer has never extended any of the 3rd party classes they will still need to spend time to understand what has changes and ponder whether if effects their code or not.
And it certainly can be, but it can also
be dangerous to put in new language features which can be misunderstood and
used inappropriately. I just think we need to carefully weigh up the
cost/benefit of new language features (particularly new syntax).
I think weighing the cost/benefit is exactly what this discussions are doing.
-Mike
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.
I'd argue that if the original developer made clear that you shouldn't
extend a class, then they still have no moral, ethical, or even contractual
obligation to care about the fact you've used the object in a way they were
clear was not supported.
I'm with David on this one. I can understand the need to enforce a
final/sealed concept for core functionality implemented in C which might do
some funny things under the hood. I don't think that should be extended to
userland. If you want to warn someone, that's fine. But don't totally
prohibit them.
Given the ability for composer to pull from forked repos and the easy of
keeping a forked repo in sync with it's upstream version, creating a fork
just to remove a sealed/final designation isn't that difficult to do.
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:
- 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 asFiveStarRating
which
could only contain 1, 2, 3, 4 or 5, or types that identify a signature, for
example asConvertToString
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.
- 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/>
--
Chase Peeler
chasepeeler@gmail.com
---- On Sun, 25 Apr 2021 22:56:26 +0100 Chase Peeler chasepeeler@gmail.com wrote ----
On Sun, Apr 25, 2021 at 11:36 AM Mike Schinkel mike@newclarity.net wrote:
On Apr 24, 2021, at 7:39 PM, David Gebler davidgebler@gmail.com wrote:
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.I'd argue that if the original developer made clear that you shouldn't
extend a class, then they still have no moral, ethical, or even contractual
obligation to care about the fact you've used the object in a way they were
clear was not supported.I'm with David on this one. I can understand the need to enforce a
final/sealed concept for core functionality implemented in C which might do
some funny things under the hood. I don't think that should be extended to
userland. If you want to warn someone, that's fine. But don't totally
prohibit them.Given the ability for composer to pull from forked repos and the easy of
keeping a forked repo in sync with it's upstream version, creating a fork
just to remove a sealed/final designation isn't that difficult to do.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:
- 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 asFiveStarRating
which
could only contain 1, 2, 3, 4 or 5, or types that identify a signature, for
example asConvertToString
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.
- 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/>--
Chase Peeler
chasepeeler@gmail.com
I can understand the need to enforce a final/sealed concept for core functionality implemented in C which might do some funny things under the hood. I don't think that should be extended to userland. If you want to warn someone, that's fine. But don't totally prohibit them.
This is not about doing "funny things under the hood", this is about declare a possible sub-types for a specific type.
Taking the ResultInterface
example from the RFC, there can only be 2 sub types for it, Success
or Failure
, so when a function declares it's return type as ResultInterface
, you know you are getting either one of these two data types, any nothing else.
If the interface would be unsealed, that guarantee doesn't exist anymore, and the library which might contain functions that operate on the Result type would become broken as now they have another type that is not being handled when unwrapping ( hence why Psl doesn't actually contain the unwrap
function mentioned in the RFC ).
given the ability for composer to pull from forked repos and the easy of keeping a forked repo in sync with it's upstream version, creating a fork just to remove a sealed/final designation isn't that difficult to do.
Yes, you can, but i don't think any maintainer is willing to waste their time looking into a weird bug resulted from new types that you created by unsealing a sealed class/interface.
If you ever had to do this, then the library you are using is flawed, as it should offer you a way to extend things when inheritance makes sense, but sometimes it doesn't ( e.g there's no reason why someone would want a new Result
type that is not Success
or Failure
, or would want to implement the ExceptionInterface
that is used within a library )
for services, I prefer to have implementations declared final, but also offer an unsealed interface to make it possible for people to decorate the implementation or rewrite their own.
If you find yourself in need to extend a class that is declared final, decorate it instead, and if there's no interface for it, maybe send a PR, forking to remove final is not the solution.
On Sat, Apr 24, 2021 at 12:55 PM Saif Eddin Gmati azjezz@protonmail.com
wrote:
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.
Hello,
To me the first sentence of the RFC is debatable:
The purpose of inheritance is code reuse, for when you have a class that
shares common functionality, and you want others to be able to extend it
and make use of this functionality in their own class.
That sounds like [abstract] base classes, which certainly permit that, but
I wouldn't state that "the purpose" of [designing] class hierarchies is
"code reuse", which can also (better?) be achieved with traits or even
simply composition
(by the way, the introduction then mentions that "PHP has the Throwable
interface, which defines common functionality between Error
and
Exception
and is implemented by both", but there is no "code reuse" in an
interface).
I also agree with others that Shape is probably not a good example, and the
ResultInterface example feels like an enum/ADT (and the FilesystemTrait
example I guess is to replace @internal
phpDoc).
I'm not saying that this RFC is bad, but probably not as convincing as it
could be.
Regards,
--
Guilliam Xavier