On Tue, Feb 6, 2024, at 4:13 PM, Григорий Senior PHP / Разработчик Web wrote:
> Btw, i agree about Javascript, but on a low level it produces the most
> clean code, because there's no types and rules. All types moved to
> TypeScript's client side compiler.
>
> JS 15 years ago ACCIDENTALLY created a pipeline. Named it "Promise". We
> spent years after to understand that while (true) and then/catch should be
> different patterns.

I assume much of this thread is a language-barrier issue, which is making it 
more hostile than it needs to be.  So let me try and expand a bit, because I am 
actually quite sympathetic to the OP's request, though not the way it's being 
made.

First of all, please don't top post.  It is considered rude on this list.  
GMail makes it a bit annoying to bottom post but it can be done.  Please do so.

Second, there's considerable prior art and discussion on the topic of error 
handling and exceptions.  In particular, I recommend this excellent article by 
Joe Duffy:

https://joeduffyblog.com/2016/02/07/the-error-model/

And this one from me:

https://peakd.com/hive-168588/@crell/much-ado-about-null

Yes, they're long, but error handling is a topic that requires more than casual 
thought.

To summarize the articles for the short of time:

* Exceptions in many languages (including PHP) are very expensive. The stack 
trace is one of the most expensive things PHP does.  This is one of the reasons 
why exceptions are terrible for flow control.
* Unchecked exceptions (where a function doesn't define what it can throw) are 
a great way to break your application in exciting and unpredictable ways.  
(This is the other reason exceptions are terrible for flow control.)
* Many people find checked exceptions cumbersome, even though they are better 
(for reasons Duffy goes into).  This is due mostly to bad design of checked 
exceptions in JVM languages.
* The real problem is that we have a channel for a success case (return value), 
a channel for catastrophic failure (exceptions), but no channel for mundane 
errors (things a responsible developer should expect and know how to handle 
gracefully).  So those get shoved into one or the other, usually an exception.

The need is for a "mundane error" channel.  I agree with this.  Different 
languages handle it in different ways.

* Go has multi-returns.
* Rust has the Result type (which is an Either monad), and an auto-propagation 
operator (?).
* PHP has union types (though that's not a deliberate design, just an emergent 
one).

One of the key differences between different approaches is whether they force 
you to handle error cases (Result type) or let you ignore errors and assume a 
happy path (Go, PHP), letting an unhappy path just explode later on.  Whether 
the language should force you to think about unhappy paths is a complex and 
subjective question that I won't delve into now beyond saying that there's 
valid arguments and use cases for both designs.

As noted in the article above, I've started using enums and union type returns 
a lot for error handling and it's pretty nice, all things considered.  That 
works today in all supported PHP verisons (8.1+).  That said, it's not perfect, 
in part because it's not standardized and there's no really good language-level 
automation around it.

If we had generics and ADTs, building a Rust-like Result type would be super 
easy, and I'd suggest we include one in the stdlib for consistency.  We'll 
probably get ADTs eventually, but generics are not something I'd bank on, so 
that is out.

HOWEVER, and this is where the important part lies, an open, lightweight, 
checked exception system is isomorphic to a Result object.  It's just 
unwrapped.  Compare these hypotheticals:

class DivByZero {}
(this could also be an enum case, but being generic for now.)

function divide(float $a, float $b): Result<int, DivByZero>
{
    if ($b === 0) return new Result::Err(new DivByZero());
   return new Result::OK($a/$b);
}

$result = divide(5, 0);
$x = match ($result) {
  is Result::OK($x) => $x,
  is Result::Err => // Do some kind of error handling.
}

vs.

function divide(float $a, float $b): int raises DivByZero
{
    if ($b === 0) raise new DivByZero();
   return new $a/$b;
}

try {
  $result = divide(5, 0);
  // Do stuff with $result, knowing it is valid.
} catch (DivByZero) {
  // Do some kind of error handling.
}

These two samples *are logically identical*, and even have mostly the same 
performance characteristics, and both expose useful data to static analyzers.  
They're just spelled differently.  The advantage of the second is that it could 
be implemented without generics.  (ADTs would be an optional nice-to-have.)  
And if the caller doesn't handle DivByZero, it would try to pass it up to its 
caller, but being checked it would require the caller to also declare that it 
can raise DivByZero.

The second option could also be improved by other syntactic sugar to make it 
easier to work with, like Rust has.  For example:

try $result = divide(5, 0) catch (DivByZero) { // Error handling that evaluates 
to a value }

Or by making null-aware operators (??, ?->, etc.) treat raised light-exceptions 
as if they were null to make pipelining easier.  Or various other ideas I'm 
just giving examples of for the moment to make the point, but let's not get off 
on that tangent.  (I would also note that I agree entirely such a system should 
only support objects, not primitives.)

This would provide a far better alternative to the "returns value on success or 
false on failure" anti-pattern throughout the stdlib, and to the common 
"returns value on success or null on failure or value not found or any other 
possible issue" pattern common in user-space code.  (No, I don't expect us to 
go back and change all of stdlib; just pointing out that it's a known language 
limitation, and this would be the solution.)

To be clear: I really like this concept and have discussed it with others 
before, using almost exactly this syntax.  I have not proposed it because my 
read of Internals lately is that there's no stomach for more type-centric 
behavior, especially with the obvious "But we already have exceptions, what's 
yer problem?" response (which is valid to a point, but also incomplete for 
reasons explained above).  The responses in this thread so far confirm that 
fear, but as an optimist I'd be very happy to be proven wrong if there is an 
appetite for improving error handling via the type system.

Absent that, union types and enums (or really any interfaced object) or a 
purpose-built Either object are the best options today, and while they're not 
ideal, they're not bad options either.

None of that logic or argument requires sh*tting on OOP as a concept or abusing 
others on the list, however.  Doing that only undermines the valid point that 
there is ample headroom to improve PHP's error handling.

--Larry Garfield

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

      • Re: [PHP... Григорий Senior PHP / Разработчик Web
        • Re: ... Alexander Pravdin
          • ... Григорий Senior PHP / Разработчик Web
            • ... Alex Wells
            • ... Robert Landers
  • Re: [PHP-DEV] Fea... Alex Wells
    • Re: [PHP-DEV... Григорий Senior PHP / Разработчик Web
      • Re: [PHP... Arvids Godjuks
        • Re: ... Григорий Senior PHP / Разработчик Web
          • ... Григорий Senior PHP / Разработчик Web
            • ... Larry Garfield
              • ... Arvids Godjuks
              • ... Jordan LeDoux
              • ... Григорий Senior PHP / Разработчик Web
              • ... Larry Garfield
              • ... Robert Landers
              • ... Григорий Senior PHP / Разработчик Web
              • ... Robert Landers
              • ... Larry Garfield
              • ... Григорий Senior PHP / Разработчик Web
              • ... Григорий Senior PHP / Разработчик Web

Reply via email to