On Sat, Sep 14, 2024, at 4:48 PM, Jordan LeDoux wrote:
> Hello internals,
>
>
> This discussion will use my previous RFC as the starting point for 
> conversation: https://wiki.php.net/rfc/user_defined_operator_overloads

Replying to the top to avoid dragging any particular side discussion into 
this...

I've seen a few people, both in this thread and previously, make the argument 
that "operator overloads are only meaningful for math, and seriously no one 
does fancy math in PHP, so operator overloads are not necessary."  This 
statement is patently false, and I would ask that everyone stop using it as it 
is simply FUD.  I don't mean just the "no one does fancy math in PHP", but 
"operators are only meaningful for math" is just an ignorant statement.

As someone asked for use cases, here's a few use cases for operator overloads 
that do not fall into the category of fancy numeric math.  Jordan, feel free to 
borrow any of these verbatim for your next RFC draft if you wish.  (I naturally 
haven't tried running any of them, so forgive any typos or bugs.  It's just to 
get the point across of each use case.)

## Pathlib

As I mentioned previously, Python's pathlib uses / to join different path 
fragments together.  In PHP, one could implement such a library very simply 
with overloads.  (A not-late-night implementation would probably be more 
performant than this, but it's just a POC.)

class Path implements Stringable
{
  private array $parts;

  public function __construct(?string $path = null) {
    $this->parts = array_filter(explode('/', $path ?? ''));
  }

  public static function fromArray(array $parts): self {
    $new = new self();
    $new->parts = $parts;
    return $new;
  }

  public function __toString() {
    return implode('/', $this->parts);
  }

  operator /(Path|string $other, OperandPosition $pos): Path {
    if ($other instanceof Path) {
      $other = (string)$other;
    }
    $otherParts = array_filter(explode('/', $path));

    return match ($pos) {
      OperandPosition::LeftSide => self::fromArray([...$this->parts, 
...$otherParts]),
      OperandPosition::RightSide => self::fromArray([...$otherParts, 
...$this->parts]),
    };
  }
}

$p = new Path('/foo/bar');
$p2 = $p / 'beep' / 'narf/poink';

## Collections

In my research into collections in other languages, I found it was extremely 
common for collections to have operator overloads on them.  Rather than repeat 
it here, I will just link to my results and recommendations for what operators 
would make sense for what operation:

https://github.com/Crell/php-rfcs/blob/master/collections/research-notes.md#operator-based-operations

## Enum sets

Ideally, we would just use generic collections for this directly.  However, 
even without generics, bitwise overloads would allow for this to be implemented 
fairly easily for a given enum.  (Again, a smarter implementation is likely 
possible with actual effort.)

enum Perm {
  case Read;
  case Write;
  case Exec;
}

class Permissions {
  private array $cases = [];

  public function __construct(Perm ...$perms) {
     foreach ($perms as $case) {
      $this->cases[$case->name] = 1;
    }
  }

  operator +(Perm $other, OperandPosition $pos): Permissions {
    $new = clone($this);
    $new->cases[$other->name] = 1;
    return $new;
  }

  operator +(Perm $other, OperandPosition $pos): Permissions {
    $new = clone($this);
    unset($new->cases[$other->name]);
    return $new;
  }

  operator |(Permissions $other,  OperandPosition $pos): Permissions {
    $new = clone($this);
    foreach ($other->cases as $caseName => $v) {
      $new->cases[$caseName] = 1;
    }
    return $new;
  }

  operator &(Permission $other,  OperandPosition $pos): Permissions {
    $new = new self();
    $new->cases = array_key_intersect($this->cases, $other->cases);
    return $new;
  }
  
  // Not sure what operator makes sense here, so punting as this is just an 
example.
  public function has(Perm $p): bool {
    return array_key_exists($this->cases, $p->name);
  }
}

$p = new Permissions(Perm::Read);
$p2 = $p + Perm::Exec;
$p3 = $p2 | new Permissions(Perm::Write);
$p3 -= Perm::Exec;
$p3->has(Perm::Read);

## Function composition

I have long argued that PHP needs both a pipe operator and a function 
composition operator.  It wouldn't be ideal, but something like this is 
possible.  (Ideally we'd use the string concat operator here, but the RFC 
doesn't show it.  It would be a trivial change to use instead.)

class Composed {
  /** @var \Closure[] */
  private array $steps = [];

  public function __construct(?\Closure $c = null) {
    $this->steps[] = $c;
  }

  private static function fromList(array $cs): self {
    $new = new self();
    $new->steps = $cs;
    return $new;
  }

  public function __invoke(mixed $arg): mixed {
    foreach ($this->steps as $step) {
      $arg = $step($arg);
    }
    return $arg;
  }

  operator +(\Closure $other, OperandPosition $pos): self {
    return match ($pos) {
      OperandPosition::LeftSide => self::fromArray([...$this->steps, $other]),
      OperandPosition::RightSide => self::fromArray([$other, ...$this->steps]),
    };
  }
}

$fun = new Composed() 
  + someFunc(...) 
  + $obj->someMethod(...) 
  + fn(string $a) => $a . ' (archived)' 
  + strlen(...);

$fun($input);  // Calls each closure in turn.

Note that there are a half-dozen libraries in the wild that do something akin 
to this, just much more clumsily, including in Laravel.  The code above would 
be vastly simpler and easier to maintain and debug.

## Units

Others have mentioned this before, but to make clear what it could look like:

abstract readonly class MetricDistance implements MetricDistance {
  protected int $factor = 1;

  public function __construct(private int $length) {}

  public function +(MetricDistance $other, OperandPos $pos): self {
    return new self(floor(($this->length * $this->factor + $other->length * 
$other->factor)/$this->factor));
  }

  public function -(MetricDistance $other, OperandPos $pos): self {
    return match ($pos) {
      OperandPosition::LeftSide => new self(floor(($this->length * 
$this->factor - $other->length * $other->factor)/$this->factor)),
      OperandPosition::RightSide => new self(floor($other->length * 
$other->factor - $this->length * $this->factor)/$this->factor)),
    };
  }

  public function __toString(): string {
    return $this->length;
  }
}

readonly class Meters extends MetricDistance {
  protected int $factor = 1;
}

readonly class Kilometers extends MetricDistance {
  protected int $factor = 1000;
}

$m1 = new Meters(500);
$k1 = new Kilometers(3);
$m1 += $k1;
print $m1; // prints 3500

$m1 + 12; // Error.  12 what?

There's likely a bug in the above somewhere, but it's late and it still gets 
the point across for now.

(Side note: The previous RFC supported abstract operator declarations, but not 
declarations on interfaces.  That seems necessary for completeness.)

## Date and time

DateTimeImmutable and DateInterval already do this, and they're not "fancy 
math."



I consider all of the above to be reasonable, viable, and useful applications 
of operator overloading, none of which are fancy or esoteric math cases.  
Others may dislike them, stylistically.  That's a subjective question, so 
opinions can differ.  But the viability of the above cases is not disputable, 
so the claim that operator overloading is too niche to be worth it is, I would 
argue, demonstrably false.

--Larry Garfield

Reply via email to