Project

General

Profile

Feature #16120

Omitted block argument if block starts with dot-method call

Added by Dan0042 (Daniel DeLorme) 25 days ago. Updated 20 days ago.

Status:
Open
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:94503]

Description

How about considering this syntax for implicit block parameter:

[10, 20, 30].map{ .to_s(16) }  #=> ["a", "14", "1e"]

Infinite thanks to maedi (Maedi Prichard) for the idea

This proposal is related to #4475, #8987, #9076, #10318, #10394, #10829, #12115, #15302, #15483, #15723, #15799, #15897, #16113 (and probably many others) which I feel are all trying to solve the same "problem". So I strongly believe all these feature requests should to be considered together in order to make a decision. And then closed together.

This "problem" can be more-or-less stated thus:

  • There is a very common pattern in ruby: posts.map{ |post| post.author.name }
  • In that line, the three 3 "post" in close proximity feel redundant and not DRY.
  • To reduce the verbosity, people tend to use a meaningless one-character variable in the block
  • But even so posts.map{ |p| p.author.name } still feels redundant.
  • This "problem" is felt by many in the ruby community, and is the reason people often prefer posts.map(&:author)
  • But that only works for one method with no arguments.
  • This results in many requests for a block shorthand than can do more.

I realize that many people feel this is not a problem at all and keep saying "just use regular block syntax". But the repeated requests over the years, as well as the widespread usage of (&:to_s), definitely indicate this is a wish/need for a lot of people.

Rather than adding to #15723 or #15897, I chose to make this a separate proposal because, unlike it or @ implicit variables, it allows to simplify only { |x| x.foo }, not { |x| foo(x) }. This is on purpose and, in my opinion, a desirable limitation.

The advantages are (all in my opinion, of course)

  • Extremely readable: posts.map{ .author.name }
    • Possibly even more than with an explicit variable.
  • Of all proposals this handles the most important use-case with the most elegant syntax.
    • It's better to have a beautiful shorthand for 90% of cases than a non-beautiful shorthand for 100% of cases.
    • A shorthand notation is less needed for { |x| foo(x) } since the two x variables are further apart and don't feel so redundant.
  • No ascii soup
  • No potential incompatibility like _ or it or item
  • Very simple to implement; there's just an implicit |var| var at the beginning of the block.
  • In a way it's similar to chaining methods on multiple lines:

    posts.map{ |post| post
      .author.name
    }
    

It may be interesting to consider that the various proposals are not necessarily mutually exclusive. You could have [1,2,3].map{ .itself + @ + @1 }. Theoretically.

I feel like I've wanted something like this for most of the 16 years I've been coding ruby. Like... this is what I wanted that (&:to_s) could only deliver half-way. I predict that if this syntax is accepted, most people using (&:to_s) will switch to this.

History

Updated by osyo (manga osyo) 25 days ago

hi.
Its soo good idea.
However, I think it is difficult to parse in the following cases.

[10, 20, 30].map{
  # 42.to_s(16)
  # or
  # pp 42
  # argument1.to_s(16)
  pp 42
  .to_s(16)
}

Updated by shan (Shannon Skipper) 24 days ago

A bit of an aside, but it's often just as fast to do two iterations with small collections, since the shorthand parses faster.

posts.map(&:author).map(&:name)

I agree with osyo that it seems this proposal collides with existing parser behavior. It would introduce incompatibility.

Updated by Dan0042 (Daniel DeLorme) 24 days ago

I think there's a misunderstanding because this proposal doesn't collide with existing parser behavior. [].each{ .method } is currently a SyntaxError.

osyo (manga osyo) wrote:

However, I think it is difficult to parse in the following cases.

It parses just like this:

[10, 20, 30].map{ |v| v
  # 42.to_s(16)
  # or
  # pp 42
  # argument1.to_s(16)
  pp 42
  .to_s(16)
}

In other words the block argument is not used, and .to_s(16) applies to 42, just like regular method chaining.

#4

Updated by Dan0042 (Daniel DeLorme) 24 days ago

  • Description updated (diff)

Updated by mame (Yusuke Endoh) 24 days ago

Hi,

So I strongly believe all these feature requests should to be considered together in order to make a decision.

Agreed. And, #15723 (a numbered parameter) is only one proposal that is all-purpose, though I don't like it so much.

A shorthand notation is less needed for { |x| foo(x) } since the two x variables are further apart and don't feel so redundant.

I personally agree. I don't think that the variable name is redundant. But people seem to think so. Actually, a shorthand for Object#method is planned for 2.7 (#12125), and I hear many people want to use it as: map(&JSON.:parse). The syntax you propose cannot absorb this style.

Updated by Dan0042 (Daniel DeLorme) 21 days ago

The syntax I propose is definitely not meant to absorb all styles. I think any attempt to be everything to everyone is doomed to failure. I do not believe this is a race where only one of the various proposals can win; considering the various proposals together means finding the right balance, not finding a single all-purpose solution.

In fact I find that map{ .to_s(16) } and map(&JSON.:parse) are very complementary...

  • map{ .to_s(16) } is shorthand for map{ |x| x.to_s(16) }; each element is the receiver of a message; this is OO style. I would use that a lot.
  • map(&JSON.:parse) is shorthand for map{ |x| JSON.parse(x) }; each element is the argument of a function; this is functional style, for people who want first-class functions in ruby. I would likely never use that. But I don't mind others who want to use that style.

Updated by mame (Yusuke Endoh) 21 days ago

I believe that map(&JSON.:parse) must be considered together because it is strongly related to the motivation of your proposal. Note that map(&JSON.:parse) is incomplete. People will next want to omit a parameter of map {|x| JSON.parse(x, symbolize_names: true) }. The game is not ended, and the next proposal will definitely come, like map(&JSON.:parse.(_, symbolize_names: true)) or what not. Only the numbered parameter can end the game.

Updated by Dan0042 (Daniel DeLorme) 21 days ago

  • Description updated (diff)

mame (Yusuke Endoh), The motivation of this proposal is related to the side-by-side proximity/repetition of x in {|x|x.foo}. Other proposals may be different. I can only guess at their true motivations. It just seems to me that the people asking for that kind of shorthand really intend to use it for {|x|x.foo} and just throw in {|x|foo(x)} because why not. But @ is similar to $_ in that it's only useful for debugging or throwaway code. {.foo} can actually be used in production code and make it clearer.

The motivation for (&JSON.:parse)... honestly it seems like it's an entirely different beast. I don't think it's only about shortening the block. I have the feeling it's really about functional programming, and that {JSON.parse(@)} would not satistify the "requirement" for first-class functions. This one seems to be more about function composition, currying and whatnot, and less about avoiding verbosity.

You make a painfully good point about how unendingly persistent these proposals are. But if the numbered parameter could really end the game, I'm quite sure there would not be so much opposition to it in #15723.

Updated by jeremyevans0 (Jeremy Evans) 21 days ago

Dan0042 (Daniel DeLorme) wrote:

mame (Yusuke Endoh), The motivation of this proposal is related to the side-by-side proximity/repetition of x in {|x|x.foo}. Other proposals may be different. I can only guess at their true motivations. It just seems to me that the people asking for that kind of shorthand really intend to use it for {|x|x.foo} and just throw in {|x|foo(x)} because why not. But @ is similar to $_ in that it's only useful for debugging or throwaway code. {.foo} can actually be used in production code and make it clearer.

I disagree. { foo(@) } and { @.foo } are not for debugging or throwaway code, they are natural replacements for { |x| foo(x) } and {|x| x.foo }. The @ single implicit parameter approach is just as clear and is significantly more flexible than this approach (lacking a better name, the omitted parameter approach).

That's not to say the omitted parameter approach is bad. In the cases it does handle, it does save a character compared to the implicit parameter approach. I don't think that character saving makes the code clearer than the single implicit parameter approach, though. In my opinion they are even in terms of clarity.

The motivation for (&JSON.:parse)... honestly it seems like it's an entirely different beast. I don't think it's only about shortening the block. I have the feeling it's really about functional programming, and that {JSON.parse(@)} would not satistify the "requirement" for first-class functions. This one seems to be more about function composition, currying and whatnot, and less about avoiding verbosity.

I think the implicit parameter approach ({JSON.parse(@)}) is a simpler and more readable approach than the dot-colon approach ((&JSON.:parse)). Especially when you start function composition ({JSON.parse(JSON.generate(@))} vs (&(JSON.:parse << JSON.:generate))). Especially when you consider things like additional block arguments and keyword arguments (:symbolize_keys) passed to the methods.

You make a painfully good point about how unendingly persistent these proposals are. But if the numbered parameter could really end the game, I'm quite sure there would not be so much opposition to it in #15723.

By "end the game", I think mame means that it is the most flexible approach, not necessarily the best approach. And it doesn't really "end the game", as it doesn't handle block or keyword arguments :).

Updated by Dan0042 (Daniel DeLorme) 21 days ago

  • Description updated (diff)
  • Subject changed from Implicit block argument if block starts with dot-method call to Omitted block argument if block starts with dot-method call

nobu (Nobuyoshi Nakada), wow, thank you so much. I never imagined it would be THAT simple to implement.
O_O @_@ m(_ _)m

But I do think it would be better with (parser_numbered_param(p, 0)) in the commit here m(_ _)m


jeremyevans0 (Jeremy Evans) wrote:

In the cases it does handle, it does save a character compared to the implicit parameter approach. I don't think that character saving makes the code clearer than the single implicit parameter approach, though. In my opinion they are even in terms of clarity.

I totally agree that "saving" a single character makes no difference. But all these proposals are not about reducing mere character count, they're about reducing... I don't know the right word... cognitive complexity? lexical redundancy? conceptual overhead? It's the reason why people propose {item.foo} even though it has zero characters less than {|x|x.foo}. It's the reason why people who use nice descriptive variable and method names can also propose {@.foo} even though it's an insignificant three character saving. It's the reason why human languages use omissions and pronouns. Allow me to make a comparison with english:

omitted   {.foo}      John went to the market and bought apples
implicit  {@.foo}     John went to the market and he bought apples
numbered  {@1.foo}    John went to the market and HE bought apples
explicit  {|x|x.foo}  John went to the market and John bought apples

There's a reason why the first form is the most natural. When people talk about a block shorthand, I really think they mean shorter in the sense of cognition, not character count (although the two are somewhat related). So rather than thinking of a 1-char saving, it's more like explicit has 2x overhead, implicit has 1x, and omitted has 0x. Yes, we're talking about a very very tiny amount of overhead, I'll grant you, but enough to have these proposals keep popping up. That's not to say the implicit parameter approach is bad, in fact I rather like it. I just happen to think the omitted approach has so much better "flow". 1x/0x = Infinity kind of thing.

I think the implicit parameter approach ({JSON.parse(@)}) is a simpler and more readable approach than the dot-colon approach ((&JSON.:parse)).

I totally agree there again. I was trying to present the perspective of functional-style first-class-function people (which I am not). Maybe trying to argue on behalf of others is a mistake in itself.

Updated by Hanmac (Hans Mackowiak) 21 days ago

Dan0042 (Daniel DeLorme) in your list about implicit and explicit you forgot { foo } depending on the method who gets the block, it might does an instance_eval thing where the block self is the block variable

i know that would need to change of the method, but this one might be possible too

#13

Updated by Dan0042 (Daniel DeLorme) 20 days ago

  • Description updated (diff)

Updated by Dan0042 (Daniel DeLorme) 20 days ago

Hanmac (Hans Mackowiak) wrote:

Dan0042 (Daniel DeLorme) in your list about implicit and explicit you forgot { foo } depending on the method who gets the block, it might does an instance_eval thing where the block self is the block variable

There was something like that in #10394, but I think it changes the semantics of the block too much. Should {foo(bar)} really be equivalent to {|v|v.foo(v.bar)} ?

Also available in: Atom PDF