Re: [RFC][Draft] Sealed Classes

From: Date: Sun, 25 Apr 2021 14:48:03 +0000
Subject: Re: [RFC][Draft] Sealed Classes
References: 1 2 3 4 5 6 7 8  Groups: php.internals 
Request: Send a blank email to internals+get-114147@lists.php.net to get a copy of this message
> On Apr 24, 2021, at 11:47 PM, Larry Garfield <larry@garfieldtech.com> wrote: > > On Sat, Apr 24, 2021, at 2:55 PM, Olle Härstedt wrote: >> 2021-04-24 21:51 GMT+02:00, Marco Pivetta <ocramius@gmail.com>: >>> On Sat, Apr 24, 2021, 21:44 Olle Härstedt <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? >>>> >>> >>> 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 > > -- > PHP Internals - PHP Runtime Development Mailing List > To unsubscribe, visit: https://www.php.net/unsub.php >

« previous php.internals (#114147) next »