On Sat, Apr 24, 2021, at 2:55 PM, Olle Härstedt wrote:
> 2021-04-24 21:51 GMT+02:00, Marco Pivetta <ocram...@gmail.com>:
> > On Sat, Apr 24, 2021, 21:44 Olle Härstedt <olleharst...@gmail.com> wrote:
> >
> >> 2021-04-24 17:59 GMT+02:00, Saif Eddin Gmati <azj...@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

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

Reply via email to