php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Request #66368 Operators should also be functions
Submitted: 2013-12-30 08:30 UTC Modified: 2022-02-13 03:01 UTC
Votes:2
Avg. Score:3.0 ± 0.0
Reproduced:0 of 0 (0.0%)
From: chriswarbo at gmail dot com Assigned: ajf (profile)
Status: Assigned Package: *General Issues
PHP Version: Irrelevant OS: Irrelevant
Private report: No CVE-ID: None
Welcome back! If you're the original bug submitter, here's where you can edit the bug or add additional notes.
If you forgot your password, you can retrieve your password here.
Password:
Status:
Package:
Bug Type:
Summary:
From: chriswarbo at gmail dot com
New email:
PHP Version: OS:

Further comment on this bug is unnecessary.

 

 [2013-12-30 08:30 UTC] chriswarbo at gmail dot com
Description:
------------
PHP's operators (+, -, *, /, ||, &&, etc.) can be tedious to use, since they are second-class citizens compared to functions. They can't be abstracted (example 1), can't be used as callbacks (example 2) and must be hard-coded (example 3).

Operators with non-symbolic names, eg. "or", "and", "xor", etc. can be defined as functions right now without breaking existing code, since their names look like valid functions and are guaranteed to be available (example 4). For consistency, it may be desirable to create synonyms for operators with symbolic names, eg. "+", "-", "*", etc. (example 5). Ambiguities can be handled trivially (example 6). Alternatively, the symbolic names could be used directly (example 7).

In every project I work on, I inevitably end up writing these as in-line lambdas, then eventually refactor them out into a library. I'm sure I'm not alone. If PHP stopped treating operators differently from functions, large amounts of boilerplate code could be scrapped and the redundant effort of wrapping these in lambdas over and over again could be spared.

Test script:
---------------
$f = 'or';
$example_1 = $g($g(TRUE,
                   $g(FALSE, TRUE)),
                FALSE);

$example_2 = array_reduce(array(TRUE, FALSE), $f, FALSE);

$example_3 = call_user_func(
  function($op) {
    return $op(TRUE, FALSE)
  },
  $f);

// Example 4
function or($x, $y) { return $x or $y; }

// Example 5
function mult($x, $y) { return $x * $y; }

// Example 6
function plus($x, $y = NULL) { return is_null($y)? +$x : $x + $y; }
function minus($x, $y = NULL) { return is_null($y)? -$x : $x - $y; }

$example_7 = call_user_func('+', 10, 5);

Expected result:
----------------
$example_1 == TRUE;
$example_2 == TRUE;
$example_3 == TRUE;
or(TRUE, FALSE) == TRUE;
mult(5, 3) == 15;
plus(10) == 10;
plus(4, 3) == 7;
minus(5) == -5;
minus(8, 7) == 1;
$example_7 == 15;

Actual result:
--------------
$example_1 -> Undefined function 'or'
$example_2 -> Undefined function 'or'
$example_3 -> Undefined function 'or'
$example_4 -> Unexpected T_LOGICAL_OR
$example_7 -> Warning: First argument is expected to be a valid callback

Patches

Pull Requests

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2013-12-30 08:32 UTC] chriswarbo at gmail dot com
Oops, $example_1 should use $f not $g.
 [2013-12-30 20:54 UTC] anon at anon dot anon
>In every project I work on

Can you provide even one practical, convincing use case for this feature?
 [2013-12-31 10:41 UTC] chriswarbo at gmail dot com
I don't have many examples to hand, since I don't have access to my previous employers' codebases and I've not been at my current job for very long. Here's what a bit of grepping on my current employer's codebase turned up.

Here's some array comparison code. "$not_f" is just the composition of "!" and "$f", but we can't compose them directly since "!" is not a function:

  $sort = function($arr) { sort($arr); return $arr; };
  foreach (array('is_null', 'is_bool', 'is_int', 'is_string') as $f) {
    if (!array_equal($sort(array_filter($array1, $f)),
                     $sort(array_filter($array2, $f)))) return FALSE;
    $not_f = function($x) use ($f) { return !($f($x)); };
    $array1 = array_filter($array1, $not_f);
    $array2 = array_filter($array2, $not_f);
  }

Note that our codebase already contains the following composition function, but it's utility is massively hampered by so much of PHP's functionality not being in functions:

  function compose($f, $g) {
    return function($x) use ($f, $g) {
      return $f($g($x));
    };
  }

A test case for the compose function contains a few (admittedly artificial) workarounds for PHP's distinction between operators and functions (closing over $a and $b isn't necessary, it just saves us a line or two since PHP doesn't support currying):

  protected function testComposeComposes() {
    $a  = mt_rand(-1000, 1000);
    $b  = mt_rand(-1000, 1000);
    $x  = mt_rand(-1000, 1000);
    $f  = function($x) use (&$a) { return $x * $a; };
    $g  = function($x) use (&$b) { return $x + $b; };
    $fg = compose($f, $g);
    $composed = $fg($x);
    $manual   = ($x + $b) * $a;
    if (!$this->assertIdentical(
      $composed,
      $manual,
      "Composed addition and multiplication $a $b $x $composed $manual")) {
      $this->dump(array(
        '$a'        => $a,
        '$b'        => $b,
        '$x'        => $x,
        '$composed' => $composed,
        '$manual'   => $manual
      ));
    }
  }

Here's a test method, which again just composes "!" with another function (Drupal's "module_exists" function) but has to do so manually:

  protected function ourDependenciesAreEnabledTest() {
    $this->assertTrue(
      count(array_filter($this->ourDependencies(),
                         function ($m) { return !module_exists($m); })) === 0,
      'All of our dependencies are enabled');
  }

Here's some code for loading all includes from a directory, with a lambda to work around the fact that "require_once" isn't a function:

  array_map(function($i) { require_once($i); },
            find_includes(TRUE));

Here's a version of "array_reduce" which passes the keys to the folding function. The first anonymous function just works around the fact that "array" isn't a function (the second is just an uncurry function):

  function array_reduce_keys($arr, $f, $i) {
    return array_reduce(
      // Pair up keys and values
      array_map_keys(
        function ($k, $v) { return array($k, $v); },
        $arr),

      // Pull apart each pair and send to $f
      function ($result, $pair) use (&$f) {
        return $f($result, $pair[0], $pair[1]);
      },

      // Start with $i
      $i);
  }

Here's a test case for the above function, which uses a lambda to work around the fact that "+" isn't a function:

protected function testArrayReduceKeysIsGivenKeys() {
    // Our keys are numeric, so try summing them
    $arr = $this->randomArray();
    $this->assertIdentical(
      array_sum(array_keys($arr)),
      array_reduce_keys($arr, function($x, $y, $_) { return $x + $y; }, 0),
      'array_reduce_keys is given access to keys');
  }

Here's a function from our payment system which tries to look up a user's billing information, which is a workaround for "->" not being a function:

  $default = function($n) use ($billing_info) {
    return $billing_info->$n;
  };

Of course, this is code which is in production *despite* PHP's asymmetric treatment of operators and functions. Most of the time I'll refactor the algorithm to work around these deficiencies (manually unrolling maps/folds/filters, hard-coding operators, copy/pasting functions just to change an operator, etc.). Such things are more difficult to grep for than lambdas ("function[ ]*(").

Most of the use-cases I found weren't quite applicable, since one of the operator arguments values was constant, for example:

  function($node) { return 'nodes/' . $node; }

  function($p, $q) {
    db_query('UPDATE {quiz_question} SET previous = :p WHERE question_id = :q',
             array(':p' => $p,
                   ':q' => $q));
  }

  function($x) { return $x? 'right' : 'wrong'; }

  function ($x) { return abs($x - 1) < 0.0001; }

  $band_codes = array_map(function($b) { return $b['band']; }, $bands);

  entity_get_controller('commerce_order')->resetCache(
      array_map(function($o) { return $o->order_id; },
                $orders));

  function($key) use ($licensee) { return $licensee[$key]; }

  function($p) { return product_display_nodes($p->product_id); }

With currying and/or partial-application of functions (like the "curry" function at http://chriswarbo.net/data_custom/prelude.txt ), these would become valid use-cases for this ticket, since they would need ".", "array" and "=>", "?!", "<", "[]", and "->" to be functions, respectively. Currying deserves a separate ticket though, and the implementation I linked to isn't intended for production use (PHP doesn't have tail-call optimisation, so I'd need to reign in its stack usage).
 [2014-01-01 09:10 UTC] krakjoe@php.net
I've read this, several times, last night and this morning. I cannot make good sense out of your request, I don't know what you're asking for. Some of this just doesn't make sense (why on earth should [] or -> be a function).

I'm sure it all makes sense to you in your head, but I'm having a hard time understanding what it is you want to be implemented. By title alone; you do not want all operators and some language constructs to be functions, that would destroy performance, obviously. You don't appear to want operator overloading because this appears focused on primitive types.

What is it exactly that you want ?

Sorry if the explanation is there, I just don't see it ...
 [2014-01-01 12:38 UTC] chriswarbo at gmail dot com
I don't think it's necessary to replace operators with functions; it's more about allowing operators to be used in the same way as functions, regardless of how they're implemented.

The crucial difference at the moment can be seen by PHP's distinction between calling named functions directly and indirectly (via a string identifier):

// Direct
my_function(10, 20);

// Indirect
$f = 'my_function';
$f(10, 20);

This is a bit clunky (compared to "$f = my_function;" in most other languages), but it gets the job done. Let's compare it to operators:

// Direct
10 + 20;

// Indirect
$o = '+';
$o(10, 20);  // Fatal error: Call to undefined function +()

This is the essence of all the above examples: we can't pass around identifiers for operators like we can for named functions. I have no objections to the distinction between operators and functions, except for this advantage that functions currently have over operators. Solving this from inside PHP requires wrapping every operator with a function, but solving it at the implementation level gives us more options to play with.

In particular, we can use PHP's 'clunky' method of indirect calls to our advantage. Since functions are identified by strings, and those strings can contain arbitrary runtime data, the PHP interpreter is forced to use a lookup procedure to find the right function. This gives us an opportunity to inject some extra logic, for example replacing the current fatal error trigger with a switch statement for each operator, defaulting to the fatal error.
 [2014-01-01 13:04 UTC] chriswarbo at gmail dot com
As for "[]" and "->" being functions (or function-like), consider the following:

function subscript($array, $index) {
  return $array[$index];
}

array_map('subscript', $my_arrays, range(0, 9));

function prop($object, $property) {
  return $object->$property;
}

array_map('prop', $my_objects, $props);

Personally I don't use the above functions, but I make heavy use of a function "lookup" which does both (based on the argument's type) plus a few bells and whistles (recursively looking up an array of identifiers, exception and error handling for magic methods, etc.).
 [2014-01-01 14:43 UTC] krakjoe@php.net
Ok, I understand a bit clearer what you're asking for ... but don't really see why; I cannot imagine a time where:

$o = '+';
$o(10, 20);

Is required such that:

$o = function($l, $r) {
  return $l + $r;
};

Does not suffice.

Maybe someone else will get it, I don't ...
 [2014-01-02 12:23 UTC] chriswarbo at gmail dot com
It is sufficient to do:

$o = function($l, $r) {
  return $l + $r;
};

But after defining such a function for the umpteenth time, I decided to raise this issue. I don't think I'm alone in using such definitions, since it's a common-enough pattern that we had array_sum built in to the language even before we had lambdas and array_reduce (array_sum is just array_reduce curried with $o and 0).

One solution would be to build in functions like $o to prevent the need to define them over and over in-line or in libraries, but I'd rather not be 'that guy' who wants his own helper-functions built-in, since adding new functions bloats the language and, more importantly, burdens developers with extra complexity ("Should I use + or $o?").

Instead, I think it's an opportunity to fix the treatment of operators, especially since functions like $o are just eta-expansions of operators, and eta-expansion is always a useless transformation:

function eta($f) {
  return function ($x) {
    return $f($x);
  };
}

call_user_func(eta($x), $y) === call_user_func($x, $y)
 [2017-09-12 13:52 UTC] cmb@php.net
FTR: a respective "Operator functions" RFC is currently under
discussion: <https://wiki.php.net/rfc/operator_functions>.
 [2017-09-12 13:52 UTC] cmb@php.net
-Assigned To: +Assigned To: ajf
 [2022-02-13 03:01 UTC] requinix@php.net
-Block user comment: No +Block user comment: Yes
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Thu Nov 21 18:01:29 2024 UTC