Re: [PHP-DEV] [RFC]

This is only part of a thread. view whole thread
  108542
February 13, 2020 16:55 larry@garfieldtech.com ("Larry Garfield")
On Tue, Feb 11, 2020, at 1:08 PM, Dik Takken wrote:

> On 11-02-2020 19:46, Larry Garfield wrote: > > > > I would love a nicer way to reference function names; it's really ugly > to do functional code in PHP otherwise, or even just dynamic function > logic within a namespace. If I never have to write $fn = __NAMESPACE__ > . '\a_func' again, it will be too soon. :-) > > Perhaps Larry can convince us all to go for something like $() by > posting a fabulous functional programming example?
I walked right into that one, didn't I... Well, Dik asked me to post a "fabulous functional programming example". I dont' have one, so I'll go with one from the book I'm working on instead. :-) This is what I have now, assuming all global functions: $result = Stats::of($string) ->analyze('normalizeNewlines') ->analyze('readingLevel') ->analyze('countStats') ->analyze(fn($s) => wordDistribution($s, 3)) ; I think we are all in agreement that is sub-optimal. If using functions in the `Stats\Analyzer` namespace, you end up with this: $result = Stats::of($string) ->analyze('\Stats\Analyzer\normalizeNewlines') ->analyze('\Stats\Analyzer\readingLevel') ->analyze('\Stats\Analyzer\countStats') ->analyze(fn($s) => \Stats\Analyzer\wordDistribution($s, 3)) ; Or if you're in that namespace (as I often am) you can do: $ns = __NAMESPACE__; $result = Stats::of($string) ->analyze($ns . '\normalizeNewlines') ->analyze($ns . '\readingLevel') ->analyze($ns . '\countStats') ->analyze(fn($s) => \Stats\Analyzer\wordDistribution($s, 3)) ; This is doubleplusungood. I'll expand the example to use some methods, too, for variety's sake: $result = Stats::of($string) ->analyze('normalizeNewlines') ->analyze('readingLevel') ->analyze('countStats') ->analyze([$synonymSuggester, 'analyze']) ->analyze([WordCouter::class, 'analyze']) ->analyze(fn($s) => wordDistribution($s, 3)) ; As I understand it, the goal in this thread is to: 1) Make code like the above more statically analyziable by not using strings. (That covers a number of areas.) 2) Make code like the above easier to write by using symbols rather than strings. In each case, the analyze() method wants a callable with a specific signature. Enforced callable signatures are *absolutely something we desperately need*, but are not the scope at the moment. In this example, then, that a function name is a string is incidental; what we care about is that it's callable. I don't have a good example off hand for it actually wanting a string, although strings are callables. So, given this import block: use Stats\Analyzer\normalizeNewlines; use Stats\Analyzer\readingLevel; use Stats\Analyzer\countStats; use Stats\Analyzer\normalizeNewlines; use Stats\Analyzer\wordDistribution; use Stats\Analyzer\Stats; use Stats\Analyzer\WordCounter; here's the above example cast into a number of the different proposals floating about this thread: ::function $result = Stats::of($string) ->analyze(normalizeNewlines::function) ->analyze(readingLevel::function) ->analyze(countStats::function) ->analyze([$synonymSuggester, 'analyze']) ->analyze([WordCouter::class, 'analyze']) ->analyze(fn($s) => wordDistribution($s, 3)) ; Analysis: I stand by my earlier statement that ::function is just too damned long for this funtionality. Not when already reserved shorter options exist. ::fn $result = Stats::of($string) ->analyze(normalizeNewlines::fn) ->analyze(readingLevel::fn) ->analyze(countStats::fn) ->analyze([$synonymSuggester, 'analyze']) ->analyze([WordCouter::class, 'analyze']) ->analyze(fn($s) => wordDistribution($s, 3)) ; Analysis: Same thing, less typing. ::name $result = Stats::of($string) ->analyze(normalizeNewlines::name) ->analyze(readingLevel::name) ->analyze(countStats::name) ->analyze([$synonymSuggester, 'analyze']) ->analyze([WordCouter::name, 'analyze']) ->analyze(fn($s) => wordDistribution($s, 3)) ; Analysis: This one gives us unification between classes and functions. I think that would be good, although it's not a hard requirement for the functionality. Any of the above could be expanded, potentially, to include methods, like so: ::name, for methods, too $result = Stats::of($string) ->analyze(normalizeNewlines::name) ->analyze(readingLevel::name) ->analyze(countStats::name) ->analyze([$synonymSuggester, analyze::name]) ->analyze([WordCouter::name, analyze::name]) ->analyze(fn($s) => wordDistribution($s, 3)) ; However, that requires the parser being able to tell the difference between analyze::name inside a callable array and not. That's... potentially complicated, since you can also just have an array that contains both objects and strings, or objects and callables, especially as objects can also be callables with __invoke(). $() or variations thereof $result = Stats::of($string) ->analyze($(normalizeNewlines)) ->analyze($(readingLevel)) ->analyze($(countStats)) ->analyze($($synonymSuggester, 'analyze')) ->analyze($(WordCouter::class, 'analyze')) ->analyze(fn($s) => wordDistribution($s, 3)) ; Analysis: I'm not sure I like this one, visually. "($(..))" feels like a lot of sigils soup. It also doesn't offer a way to deal with the method names in the object versions, unless we just assume that bare strings are allowed there, like so: $result = Stats::of($string) ->analyze($(normalizeNewlines)) ->analyze($(readingLevel)) ->analyze($(countStats)) ->analyze($($synonymSuggester, analyze)) ->analyze($(WordCouter::class, analyze)) ->analyze(fn($s) => wordDistribution($s, 3)) ; I will say that, given the behavior of ::class now, ::fn or ::name "feel like" they should return a string, whereas $() "feels like" it should return a callable, or something richer than a string. That's naturally subjective but is consistent with ::foo being a constant value and $() being, um, jQuery. Also suggested was this: closure() $result = Stats::of($string) ->analyze(closure(normalizeNewlines)) ->analyze(closure(readingLevel)) ->analyze(closure(countStats)) ->analyze(closure($synonymSuggester->analyze)) ->analyze(closure(WordCouter::analyze)) ->analyze(fn($s) => wordDistribution($s, 3)) ; Which aside from being verbose really does look an awful lot like a function call, and is how I'd interpret it. That said, a syntax that would recognize `$foo->bar` as a callable, not an invocation, and `Foo::bar` as a callable, would be really nice. Of course... I feel compelled to ask why we can't just use bare function names. Treating a bare string as a string has been deprecated for several versions. If we remove that in PHP 8 and instead let it mean constant, then function, then class name, the following would become legal: $result = Stats::of($string) ->analyze(normalizeNewlines) ->analyze(readingLevel) ->analyze(countStats) ->analyze([$synonymSuggester, analyze]) ->analyze([WordCouter, analyze]) ->analyze(fn($s) => wordDistribution($s, DISTRIBUTION_LIMIT)) ; Which would be much more in line with how many other languages handle symbol names. (There is likely some engine reason why it's way harder than I make it sound; I'd love to hear what that is so I know not to suggest it again, unless it really is that simple in which case...) In all, I don't think there's a clear obvious winner here. All of the options have some kind of trade-off. My own preferences lean toward: 1) Keep it short; we don't need more long words. 2) No more parentheses; in context, there will be ample parentheses already anywhere a function is being referenced as a callable, so let's not drift more toward LISP. (This is why I don't think "well wrap it in fromCallable()" is a good answer.) 3) Handling method callables at the same time is *not* required, but would be really good for a complete solution. --Larry Garfield
  108554
February 13, 2020 23:13 d.h.j.takken@freedom.nl (Dik Takken)
On 13-02-2020 17:55, Larry Garfield wrote:
> I walked right into that one, didn't I...
You did. :)
> Well, Dik asked me to post a "fabulous functional programming example". I dont' have one, so I'll go with one from the book I'm working on instead. :-)
Thanks, much appreciated.
> $() or variations thereof > > $result = Stats::of($string) > ->analyze($(normalizeNewlines)) > ->analyze($(readingLevel)) > ->analyze($(countStats)) > ->analyze($($synonymSuggester, 'analyze')) > ->analyze($(WordCouter::class, 'analyze')) > ->analyze(fn($s) => wordDistribution($s, 3)) > ; > > Analysis: I'm not sure I like this one, visually. "($(..))" feels like a lot of sigils soup. It also doesn't offer a way to deal with the method names in the object versions, unless we just assume that bare strings are allowed there, like so: > > $result = Stats::of($string) > ->analyze($(normalizeNewlines)) > ->analyze($(readingLevel)) > ->analyze($(countStats)) > ->analyze($($synonymSuggester, analyze)) > ->analyze($(WordCouter::class, analyze)) > ->analyze(fn($s) => wordDistribution($s, 3)) > ;
If the $() construct would only accept methods then it provides an environment without the usual ambiguity and we can write: $result = Stats::of($string) ->analyze($(normalizeNewlines)) ->analyze($(readingLevel)) ->analyze($(countStats)) ->analyze($($synonymSuggester->analyze)) ->analyze($(WordCouter::analyze)) ->analyze(fn($s) => wordDistribution($s, 3)); The RFC of Michał (https://wiki.php.net/rfc/short-closures) would yield: $result = Stats::of($string) ->analyze({normalizeNewlines}) ->analyze({readingLevel}) ->analyze({countStats}) ->analyze({$synonymSuggester->analyze}) ->analyze({WordCouter::analyze}) ->analyze(fn($s) => wordDistribution($s, 3)); To me this looks less soup-ish compared to using $().
> I will say that, given the behavior of ::class now, ::fn or ::name "feel like" they should return a string, whereas $() "feels like" it should return a callable, or something richer than a string. That's naturally subjective but is consistent with ::foo being a constant value and $() being, um, jQuery.
Although a closure would also be a constant I share your 'feel like' issue. It might just need getting used to.
> Of course... I feel compelled to ask why we can't just use bare function names. Treating a bare string as a string has been deprecated for several versions. If we remove that in PHP 8 and instead let it mean constant, then function, then class name, the following would become legal: > > $result = Stats::of($string) > ->analyze(normalizeNewlines) > ->analyze(readingLevel) > ->analyze(countStats) > ->analyze([$synonymSuggester, analyze]) > ->analyze([WordCouter, analyze]) > ->analyze(fn($s) => wordDistribution($s, DISTRIBUTION_LIMIT)) > ; > > Which would be much more in line with how many other languages handle symbol names. (There is likely some engine reason why it's way harder than I make it sound; I'd love to hear what that is so I know not to suggest it again, unless it really is that simple in which case...)
I guess using bare function names in that context could be supported. I would prefer getting rid of these awkward array constructs altogether though and just write $synonymSuggester->analyze in stead. This requires a context that only accepts methods, which is what $() or {} could provide. As a side note, I would love to have a name for these closure producing constructs we are talking about. Maybe: 'enclosure'? Regards, Dik Takken