On Wed, Feb 7, 2024, at 12:55 AM, Alex Wells wrote:
> On Tue, Feb 6, 2024 at 7:14 PM Larry Garfield <la...@garfieldtech.com>
> wrote:
>
>> 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.
>>
>
> Let's assume that the developer knows the divisor isn't 0 - through an
> assertion or an `if` clause above the call to `divide(5, $divisor)`. In
> this case, DivByZero error cannot ever be thrown (or risen), but the
> developer would still have to either handle the error (which will never
> happen) or declare it as raisable, which in turn may require also marking
> 10+ function/method calls as "raises DivByZero". Both options aren't great.
>
> And even if there was no assertion about the divisor, maybe the developer's
> intent is exactly to ignore that case as an "implicit assertion" - meaning
> instead of explicitly asserting the divisor value themselves (through
> `assert($divisor !== 0)`), they rely on `divide(5, $divisor)` doing that
> implicitly for them. If the `assert()` fails, then nobody is expected to
> really handle that assertion error; it usually bubbles up to the global
> exception handler which takes care of it. If the `divide()` fails on the
> other hand, checked exceptions would require all the callers to actually
> "check" it by catching or declaring the caller function as `raises
> DivByZero`, but this doesn't bring any benefit to the developer in this
> case.
>
> So I assume this is why Java developers hate checked exceptions and why
> Kotlin doesn't have them. I'm not aware of other implementations of checked
> exceptions; there may be other, better versions of them. If you have any in
> mind that overcome the issues above, I'd be interested to look into them :)

Re assertions: The problem with assertions is they can be disabled.  They're 
really *only* useful as an extra "extended type check", and then only in dev.  
That makes them unreliable, so using them for flow control is right out.  And 
in practice they just turn into exceptions anyway (or Throwables at least), so 
there's really no benefit over just using a Throwable if you're going to insist 
they aren't disabled for the code to work.

The Joe Duffy article I linked above describes the issues with Java's exception 
design.  Mainly, it's only mostly-checked.  It forces you to declare your 
throwables... but certain types of throwables don't need to be declared, which 
means as a consumer of a function, you have no guarantee that a function that 
has no declared throws will actually never throw.  So you get all the pain, 
none of the gain.  (This is admittedly a challenge for introducing them to PHP 
as well, which is why I am proposing a separate syntax from exceptions since 
they would serve a different purpose.)

As discussed in the article, Midori (the experimental language for which Duffy 
was tech lead) had checked exceptions that worked essentially as I have 
proposed here.  What made them work is

* They were very lightweight.
* They were firmly and strictly checked, without any "holes" in the design like 
Java.
* They were used locally, as an unwrapped Either monad, rather than for 
up-the-stack communication.
* Midori had a much more robust type checker than Java, so more errors could be 
moved to the type system and eliminated entirely.
* The built-in type hierarchy was more sensible than Java's.
* Midori has guards, which eliminate 99% of cases.  It's essentially promoting 
assertion-esque type checking into the function signature.  That is, DivByZero 
wouldn't even be an exception, it would be a runtime enforced type error.  I'd 
love to have these, too, but that's not the topic right now. :-)

Guards would look something like this (using Midori-inspired syntax):

function divide(float $a, float $b): float require $b !== 0 ensures return != 
INF { 
    // ...
}

(In Midori, those could either be materialized into code or compiled away if 
the compiler could guarantee they held true.  In PHP I don't think we could 
compile them away so they'd have to be materialized, but it would make them 
more apparent to static analyzers as well as better communicate intent to other 
developers.)

The article goes into much more detail, and I really do encourage reading it.

To your specific question about prior knowledge (eg, non-zero), there's a 
couple of ways, conceptually, to address that.

1. A more robust type system can handle things like non-zero-int or 
unsigned-int as a type.  (I believe Midori has this, but honestly it's unlikely 
for PHP.)
2. Guard clauses.
3. Better syntax making handling "no op errors" easier.

For the third, just to spitball:

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

$result = try divide(5, 0) on DivByZero null;
// Equivalent to:

try {
  $result = divide(5, 0);
} catch (DivByZero) {
  $result = null;
}

But the basic point is that DivByZero is probably a bad use case example as 
that should be an Abandonment case (ie, type failure), just the easiest one I 
came up with on the spot. :-)

To use the more practical example, 

findProduct(int $id): Product raises ProductNotFound {

}

function mycontroller(int $id) {
    $product = try findProduct($id) on ProductNotFound return view('not_found');

    return view('product', $product);
}

If you're 100% certain the ID is valid, you could do "on ProductNotFound null" 
as a no-op case.  However, I suspect in practice that is a minority case.

Routing would probably be more like this:

function findRoute(Request $request): Route raises RoutingError

try $route = findRoute($request);
catch (RouteNotFound $r) {
    // Do stuff.
} catch (MethodNotAllowed $r) {
    // Do stuff.
}
// ...

(Though in fairness, I'd probably use a proper monad for routing instead 
anyway.)

--Larry Garfield

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

Reply via email to