5 ms·
The main pain-point I have with PHP is how clunky its functions are; it seems like first-class functions have only been added so they can tick a box on their fe
by chriswarbo 13y ago
The main pain-point I have with PHP is how clunky its functions are; it seems like first-class functions have only been added so they can tick a box on their feature list, since many aspects of PHP make them unnecessarily hard to use.
Passing function names around as strings is, of course, a hideous consequence of using separate namespaces for functions (foo) and variables ($foo), so it's at least understandable.
Having to explicitly list a function's free variables with "use" is a hideous consequence of not having lexical scope from the start, so again it's understandable.
What I don't understand is why functions aren't first-class in PHP's parser. For example, the parser doesn't know that functions may be returned from other functions:
$foo = function() { return function() { echo "Hello;" }; };
$foo()(); // Parse error
Instead we have to perform some indirection like this:
call_user_func($foo());
Or like this:
$bar = $foo();
$bar();
Likewise, the parser doesn't realise that functions may be stored in objects:
$baz = new stdClass;
$baz->quux = $foo();
$baz->quux(); // Error, no such method
Again, we have to add an indirection:
call_user_func($baz->quux);
Or
$foobar = $bar->quux;
$foobar();
Named functions can't be closures, even though we can declare them anywhere, presumably for historical reasons, ie. this is a syntax error:
function foo($bar) {
function baz() use ($bar) {
echo $bar;
}
}
Another example of making it unnecessarily hard to use functions is iterable objects, which can only be iterated via foreach loops (ie. there is no way to map, reduce or filter them).
Of course, I could also complain about no tail-call optimisation, functions not being curried, etc. but those aren't PHP-specific since very few scripting languages seem to get them right for some reason :(
- Udo 13y agoYou're right that the parser would benefit from some more work to make edge cases like $foo()() work correctly. It'd be the single most important feature for a 6.x version as far as I'm concerned.
- chriswarbo 13y agoIt wouldn't need to be a version 6 feature, since it wouldn't break backwards-compatibility, but it may end up having to wait if that part of the interpreter is particularly gnarly (I've played with PHP's C code and I could see why it may be difficult!). Also, I'd hesitate to call "$foo()()" an 'edge case', since that implies that it's somehow different to any other function call. Making such a distinction is exactly what PHP got wrong here, ie. the parser should allow any expression to be called as a function, including other function calls and property lookups. Presumably there are two reasons why this happened: 1) Languages with first-class functions (and lexically-scoped closures) have fewer distinct features than languages without them; ie. functions are no longer distinct from other values (ints, bools, etc.), function names are no longer distinct from variable names (foo vs. $foo), methods are no longer distinct from properties ($foo->bar() could be a method call or a function call), functions and classes begin to overlap (lexical scope competes with instantiation and inheritance for resolving free variables) 2) For performance reasons, when these features were still distinct, they were branched on as early as possible, ie. in the parser. Essentially the problem is that the parser is still making distinctions which first-class functions have unified. The solution is to delay committing to these branches until further down the pipeline, ie. using a generic "expression" token, which "$foo" and "<expression>()" both parse to. The action to take can be resolved at runtime (when the value of the expression is known). In other words, there should be no difference between "<token>(<args>)" and "call_user_func(<token>, <args>)" (except maybe for things like pass-by-reference).
- Udo 13y ago> Making such a distinction is exactly what PHP got wrong here, ie. the parser should allow any expression to be called as a function, including other function calls and property lookups. I didn't imply that I would have done it the same way they did! I merely expressed the feeling that the parser might be implemented in a way that makes this hard to fix without an extensive rewrite. From the parser's perspective this might be an edge case. Admittedly, you know more about PHP internals than I do, but I assumed there would be hurdles in there - otherwise this should have been done years ago. Especially since I expect there is no compatibility downside in making these cases work. Working with PHP I got the same impression as you did, namely that the parser makes certain assumptions about the code structure that causes the result to be more rigid. I agree that ideally the parser should just be agnostic about stuff it can't meaningfully reason about.
- nubs 13y agoThere are people trying to take care of some of that already. https://github.com/php/php-src/pull/301 https://github.com/php/php-src/pull/301 was at some point hoping to get into 5.6 but I don't know what happened to it since then.
- jtreminio 13y agoA well-reasoned argument about pain points you've experienced with php! I have also been bitten by this non-feature and wish I had the time to learn C well enough to be able to contribute towards these sorts of things.
- trebor 13y ago> Having to explicitly list a function's free variables with "use" is a hideous consequence of not having lexical scope from the start, so again it's understandable. No, it's a clear avoidance of the problems of the Javascript lexical scope. Yes, it'd be nice to have that "for free", but there's far worse that could happen if you had free access to all variables. The GC complexity alone would outweigh the benefit. > Likewise, the parser doesn't realise that functions may be stored in objects: A contrived example actually. First, the interpreter only interprets "$foo->quux()" as a method call on a non-existent method. You are not accessing a property of the object, ever, through a method call like that. Instead, you have the option to implement __call. class foo { public $quux; public function __call($method, $args) { if (property_exists($this, $method) && is_callable($this->$method)) { return call_user_func_array($this->$method, $args); } } } $foo = new foo; $foo->quux = function () { echo 'hello'; }; $foo->quux(); Changing this behavior could single-handedly break compatibility with all libraries making use of __get/__call methods. It could have been implemented, yes, and probably should have been. However, most people using PHP aren't throwing closures on an object (the closure won't have access to $this scope till 5.4 or later, you know) and expecting them to work like a method. I may address the other points later, but I'm pretty sure that if you'd take a look and try writing idiomatic PHP you'd find it easier than assuming that your idioms from other languages will work directly and without modification.
- chriswarbo 13y ago> No, it's a clear avoidance of the problems of the Javascript lexical scope. Do you mean problems with the way Javascript implements lexical scope (no "let" blocks until recently, hoisting, global-by-default, etc.) or problems with lexical scope in general? Lexical scope's been around since at least ALGOL60, but really started to shine in Scheme. I agree that there's a tradeoff for GC, and as I said it's understandable why "use" acts the way it does, since lexical scope wasn't in there from the start (where it could have informed other scoping policies). > A contrived example actually. I agree, but it's contrived precisely because PHP's OO and functional features don't play well together, so everybody's learned not to do it. > First, the interpreter only interprets "$foo->quux()" as a method call on a non-existent method. You are not accessing a property of the object, ever, through a method call like that. That's exactly my point! :) > Instead, you have the option to implement __call. Yes, this is a decent workaround for a self-contained library/app/etc. but it shouldn't really be necessary. > Changing this behavior could single-handedly break compatibility with all libraries making use of __get/__call methods. That's an issue, but I don't think it would be as calamatous as you say :) It would mainly alter behaviour for those who are putting functions in their objects, but as you say that's not commonly done! > However, most people using PHP aren't throwing closures on an object (the closure won't have access to $this scope till 5.4 or later, you know) and expecting them to work like a method. I wasn't saying that a function property should act like a method semantically (ie. be called with an implicit $this argument), only that it should be possible to call it like a method syntactically, ie. the syntax "A(B)" should call "A" with arguments "B". Note that it's not just functions; quux could be anything callable. > I'm pretty sure that if you'd take a look and try writing idiomatic PHP you'd find it easier than assuming that your idioms from other languages will work directly and without modification. I've been writing PHP since 4.2, so I'm perfectly aware of what does and does not work. I'm pointing out what could work, and make PHP much nicer. Sticking to standard practice is a good idea when writing libraries and applications, but is stifling when brainstorming for a new version of PHP. My problem with idiomatic PHP is that it's usually incredibly verbose, usually because everything's been collapsed down to be first-order, which leads to boilerplate (eg. repeated build-up and tear-down code) and copypasta ("multiply_all_foos" and "sum_all_foos" which are the same except for the operator, or "Foo->multiply" and "Foo->sum" if you prefer). A few higher-order functions, like function composition and partial application, can cut down this bloat tremendously. It's just jarring when this bumps into a completely unnecessary PHP quirk, like having to put "call_user_func" all over the place ;)