Project

General

Profile

Actions

Feature #12125

open

Proposal: Shorthand operator for Object#method

Added by Papierkorb (Stefan Merettig) over 8 years ago. Updated almost 3 years ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:74036]

Description

Hello,

The & operator lets one pass a #call-able object as block.

Really useful feature, but at the moment, if you want to pass a Method this way the syntax is not really concise:

Dir["*/*.c"].map(&File.method(:basename))

More often than not, at least I end up writing this instead .map{|a| File.basename a} which isn't that great either.

Thus, I want to propose adding a short-hand operator to the ruby language, which simply calls #method on an Object.

It could look like this: an_object->the_method which is 100% equivalent to doing an_object.method(:the_method)

I'm reusing the -> operator which is already used for the stabby lambda. But I think it makes sense: You have an object,
and from that object you point at a method to get it as Method.

With this, the example from above becomes: Dir["*/*.c"].map(&File->basename)

I attached a proof of concept patch. When you apply this to trunk, you can try the example above yourself.
Do note however that this PoC also breaks stabby lambda for the moment. I'll work on fixing that the following
days.

Thank you for reading,
Stefan.


Files

method_shorthand.diff (740 Bytes) method_shorthand.diff Papierkorb (Stefan Merettig), 02/28/2016 09:36 PM
dot-symbol.patch (554 Bytes) dot-symbol.patch mame (Yusuke Endoh), 03/14/2016 04:43 PM

Related issues 2 (0 open2 closed)

Related to Ruby master - Feature #16275: Revert `.:` syntaxClosedActions
Has duplicate Ruby master - Feature #13581: Syntax sugar for method referenceClosedActions

Updated by Papierkorb (Stefan Merettig) over 8 years ago

The & operator lets one pass a #call-able object as block.

I meant #to_proc-able objects, sorry for the confusion.

Updated by matz (Yukihiro Matsumoto) over 8 years ago

I like the idea of short hand notation for Object#method(), but I don't think -> is a good idea.

Matz.

Updated by zverok (Victor Shepelev) over 8 years ago

For this kind of "conceptual" methods I sometimes define just one-letter shortcuts in core_ext.rb, like .map(&File.m(:basename)). I'm not sure, though, if any of existing/popular libraries will struggle from such kind of solution (or may be it would not play really well with local variables, which are frequently have one-letter names).

Updated by Papierkorb (Stefan Merettig) over 8 years ago

Yukihiro Matsumoto wrote:

but I don't think -> is a good idea.

Other options I can think of are:

  • Using .>: the_object.>a_method While its look is nearer to a normal method call (Which I think is a plus), I fear that the period would be hard to see in some fonts. Another plus is that this would be a entirely new operator, so no (unintentional?) breaking changes in the parser.
  • Using <..>: the_object<a_method> Inspired by the look of generics/templates in other programming languages. Should not clash with existing code and parsers and is well-readable in fonts I guess. Maybe it doesn't read as well anymore though, and the intention of <..> may not be that clear at first to someone who doesn't know the syntax (yet). No idea if that is a concern.
  • Using &>: the_object&>a_method Also readable in any font I can think of. It's a spin on &., reading like "and this"
  • Using |>: the_object|>a_method I think the Elixir language has this operator too (albeit with other semantics?). Its read like "and pipe it through this", so maybe it reads a bit like a shell script too?

Regards,
Stefan

Updated by ksss (Yuki Kurihara) over 8 years ago

How about this one?

class UnfoundMethod
  def initialize(receiver)
    @receiver = receiver
  end

  def method_missing(name, *args, &block)
    @receiver.method(name)
  end
end

module UnfoundMethodAttacher
  def method(name=nil)
    if name
      super
    else
      UnfoundMethod.new(self)
    end
  end
end

Object.prepend UnfoundMethodAttacher

File.method #=> #<UnfoundMethod receiver=File>
File.method.basename #=> #<Method: File.basename>
Dir["*/*.c"].map(&File.method.basename) #=> ["foo.c", "bar.c"]

Updated by funny_falcon (Yura Sokolov) over 8 years ago

Please don't do this!!! No need to make the language more complex just to solve such small issue!!!

If you want to do something pretty, then whole closure syntax should be simplified, not just call to '#method'.

Dir["*/*.c"].map{File.basename(_0)} # where `_0` is magic var

Then bytecode compiler may optimize it to .map(&File.method(:basename))

But it then allows to do more pretty things, for example:

# dumps key=>value pairs
myhash.each{|k,v| puts "#{k}=>#{v}"}
# do it in shorter way
myhash.each{puts "#{_0}=>#{_1}"}

Updated by shevegen (Robert A. Heiler) over 8 years ago

I think the &File->basename looks confusing since we also have
-> standalone now.

object->method reminds me a lot of php/perl and ruby uses the
prettier . instead, object.method.

I also think that :

  .map{File.basename(_0)} # where `_0` is magic var

Is not good either. I like _ as a variable name a lot but on
its own, not with extra. :)

Yuki Kurihara's proposal is somewhat better as he does not
have to use special constructs/tokens.

Dir["*/*.c"].map(&File.method.basename)

I believe that crystal allows some parameter for &; and I think
there have been earlier proposals in ruby too.

But I think it makes sense: You have an object, and from that
object you point at a method to get it as Method.

I do not think that this argument is a good one because the ->
is used in a dissimilar way, akin to Proc.new / lambda,
whereas your syntax proposal would be more similar to php
and perl syntax style, which I think will be confusing in
addition to -> already having another method. So this is
not good in my opinion.

Updated by nobu (Nobuyoshi Nakada) over 8 years ago

Stefan Merettig wrote:

  • Using .>: the_object.>a_method
  • Using <..>: the_object<a_method>

These two conflict with existing syntax and break compatibility.
Note that object.>(other) is a valid method call.

Updated by nobu (Nobuyoshi Nakada) over 8 years ago

  • Description updated (diff)

Updated by jwmittag (Jörg W Mittag) over 8 years ago

A proposal that has existed for years, if not decades, is to deprecate usage of the :: double colon binary infix namespace operator for message sends, and instead re-use it for method references:

Dir["*/*.c"].map(&File::basename)

This is also the syntax chosen by Java for method references.

There is one big problem, though: ambiguity with constant references for methods which start with an uppercase letter. Maybe, it would be possible to require parentheses in that case?

%w[1 2 3].map(&::Integer())

Updated by jwmittag (Jörg W Mittag) over 8 years ago

It would be nice if we could find symmetric syntax for getting an UnboundMethod from a module.

Updated by mame (Yusuke Endoh) over 8 years ago

+1 for this proposal. How about recv.:fname?

$ ./miniruby -e 'p Dir["*/*.c"].map(&File.:basename)'
["hypot.c", "memcmp.c", "erf.c", ...]

Fortunately, it brings no conflict.

A patch is attached. (dot-symbol.patch)

--
Yusuke Endoh

Updated by nobu (Nobuyoshi Nakada) over 8 years ago

Yusuke Endoh wrote:

+1 for this proposal. How about recv.:fname?

dot_or_colon means that File:::basename also works?

Updated by mame (Yusuke Endoh) over 8 years ago

Nobuyoshi Nakada wrote:

Yusuke Endoh wrote:

+1 for this proposal. How about recv.:fname?

dot_or_colon means that File:::basename also works?

Yes it works. But I don't think it is important. Only .: is also okay to me.

--
Yusuke Endoh

Updated by Papierkorb (Stefan Merettig) over 8 years ago

Yusuke Endoh wrote:

Nobuyoshi Nakada wrote:

Yusuke Endoh wrote:

+1 for this proposal. How about recv.:fname?

dot_or_colon means that File:::basename also works?

Yes it works. But I don't think it is important. Only .: is also okay to me.

The tetris-operator .: makes sense to me, but in respect to ::: I don't think we should allow "alternative" operators to do the same thing.

Updated by funny_falcon (Yura Sokolov) over 8 years ago

-1000

Please, don't!!!

I don't wonna Ruby to become Perl!!!

No more unnecessary syntax!!!

You all are not so weak! you are strong humans!!

You just can type a bit more characters!!!

Updated by hanachin (Seiei Miyagi) over 8 years ago

How about File[.basename] ?

Updated by shyouhei (Shyouhei Urabe) over 8 years ago

Just a status update: I heard from Matz in this month's developer meeting that however he wants this, all proposed syntax so far didn't charm him.

Updated by nobu (Nobuyoshi Nakada) over 8 years ago

Seiei Miyagi wrote:

How about File[.basename] ?

It causes confusion if the receiver has #[] method.

Updated by VeryBewitching (RP C) over 8 years ago

Stefan Merettig wrote:

Hello,

The & operator lets one pass a #call-able object as block.

Really useful feature, but at the moment, if you want to pass a Method this way the syntax is not really concise:

Dir["*/*.c"].map(&File.method(:basename))

More often than not, at least I end up writing this instead .map{|a| File.basename a} which isn't that great either.

Thus, I want to propose adding a short-hand operator to the ruby language, which simply calls #method on an Object.

It could look like this: an_object->the_method which is 100% equivalent to doing an_object.method(:the_method)

I'm reusing the -> operator which is already used for the stabby lambda. But I think it makes sense: You have an object,
and from that object you point at a method to get it as Method.

With this, the example from above becomes: Dir["*/*.c"].map(&File->basename)

I attached a proof of concept patch. When you apply this to trunk, you can try the example above yourself.
Do note however that this PoC also breaks stabby lambda for the moment. I'll work on fixing that the following
days.

Thank you for reading,
Stefan.

Dir["/.c"].map(File[&:basename])

From File, employ basename method. The referencing & should be applied to the method, not the class, as it is really the method you're concerned with. I'm not a language designer, but this is how I would expect this to work by looking at it.

Updated by VeryBewitching (RP C) over 8 years ago

Another thought: Dir["/.c"].map(File.&basename)

Updated by vassilevsky (Ilya Vassilevsky) about 7 years ago

Is it possible to use a single colon for this?

object:name

File:basename

URI:parse

As far as I can see (not far, really, I don't even know C), it is currently not used for anything.

Updated by nobu (Nobuyoshi Nakada) about 7 years ago

A colon does too many things already, a ternary operator, a symbol literal, and a keyword argument.

Updated by nobu (Nobuyoshi Nakada) about 7 years ago

File[&:basename] and File.&basename are valid syntax already.

Actions #25

Updated by k0kubun (Takashi Kokubun) about 7 years ago

  • Has duplicate Feature #13581: Syntax sugar for method reference added
Actions #26

Updated by nobu (Nobuyoshi Nakada) almost 6 years ago

  • Status changed from Open to Closed

Applied in changeset trunk|r66667.


Method reference operator

Introduce the new operator for method reference, .:.
[Feature #12125] [Feature #13581]
[EXPERIMENTAL]

Actions #27

Updated by znz (Kazuhiro NISHIYAMA) almost 5 years ago

Updated by cvss (Kirill Vechera) over 3 years ago

Since the proposed feature was reverted in #16275, maybe this issue should be re-opened to continue discussion and "re-introduce with a big picture".

And I'd like add another perspective:

upcased = somehash.each_pair.map{|key, value| [key.upcase, value.upcase]}.then(&Hash.:[])

instead of mixed chaining and parentheses

upcased = Hash[somehash.each_pair.map{|key, value| [key.upcase, value.upcase]}]

or a bit ugly chaining with #method

upcased = somehash.each_pair.map{|key, value| [key.upcase, value.upcase]}.then(&Hash.method(:[]))

Updated by nobu (Nobuyoshi Nakada) over 3 years ago

  • Status changed from Closed to Open

Re-opened as this feature has been reverted.

Updated by marcandre (Marc-Andre Lafortune) over 3 years ago

Since then, we have _1:

Dir["*/*.c"].map{File.basename(_1)}

My understanding is that a syntax for method would also be less performant.

@cvss (Kirill Vechera)' example with Hash#[] is not convincing as using to_h is simply better.

Updated by cvss (Kirill Vechera) over 3 years ago

@marcandre (Marc-Andre Lafortune), comparing performance, the #method way is better:

The map{File.basename(_1)} code has two performance leaks: 1) it creates a new block to pass it to 'map', 2) this block looks up the basename method on each iteration

The map(&File.method(:basename)) passes directly the method without creating an intermediate block and the basename is looked up only once while expanding the argument of map.

a = ['aa/bb']*10000
Benchmark.measure { 100.times{ a.map{|a| File.basename(a)} } } # 0.400000   0.000000   0.400000 (  0.411006)
Benchmark.measure { 100.times{ a.map(&File.method(:basename)) } } # 0.310000   0.010000   0.320000 (  0.330659)

Updated by cvss (Kirill Vechera) over 3 years ago

I agree, to_h is better, but for hashes only. When we have no such a shorthand with other classes or other constructors, we can:

some_even_set = some_array&.select(&:even?)&.then(&Set.:new)

After writing some code in 2.7.0 with .: I feel that

[1, 2, 3].any?(&some_even_set.:include?)

is more clean than

[1, 2, 3].any?{|x| some_even_set.include? x}
[1, 2, 3].any?{some_even_set.include? _1}

Updated by marcandre (Marc-Andre Lafortune) over 3 years ago

cvss (Kirill Vechera) wrote in #note-31:

@marcandre (Marc-Andre Lafortune), comparing performance, the #method way is better:

The map{File.basename(_1)} code has two performance leaks: 1) it creates a new block to pass it to 'map', 2) this block looks up the basename method on each iteration

My understanding is that 1) has no cost as the block is never captured 2) indeed plays a (small) role

The map(&File.method(:basename)) passes directly the method without creating an intermediate block and the basename is looked up only once while expanding the argument of map.

Right, but there needs to be a Method object created, which is non negligible.

Using benchmark-ips improves the reliability. Here are the results I get:

# 10000 elements (as above):
              method:      364.4 i/s
               block:      322.4 i/s - 1.13x  (± 0.00) slower
# 1000 elements:
              method:     3624.2 i/s
               block:     3212.6 i/s - same-ish: difference falls within error
# 100 elements:
              method:    35250.7 i/s
               block:    33684.3 i/s - same-ish: difference falls within error
# 10 elements:
               block:   320628.1 i/s
              method:   249288.9 i/s - 1.29x  (± 0.00) slower
# 3 elements:
               block:   956542.2 i/s
              method:   477741.7 i/s - 2.00x  (± 0.00) slower

I would venture to say that most loops are done with arrays much smaller than 1000 elements.

Here is the code I used:

gem 'benchmark-ips'
require 'benchmark/ips'

[10000, 1000, 100, 10, 3].each do |n|
  a = ['aa/bb']*n
  puts "#{n} elements"
  Benchmark.ips do |x|
    x.report('block')   {
      a.map{|a| File.basename(a) }
    }
    x.report('method')   {
      a.map(&File.method(:basename))
    }
    x.compare!
  end
end

Updated by marcandre (Marc-Andre Lafortune) over 3 years ago

@cvss (Kirill Vechera), your two other examples are also not convincing. Here is how one can write them:

some_even_set = some_array.select(&:even?).to_set

[1, 2, 3].any?(some_even_set)

Updated by Azuma-01 (Azuma Eleth) almost 3 years ago

Hello,
I cannot help myself to see than all proposal are of the form:
(expression) (shorthand_operator) (method_name)

while I had seen some interesting option, to me, that format seem to add an obsure behaviour rater than a shorthand.
Personaly, when i mix the words 'Ruby' and 'shorthand', i see %string
%i, %q, %r, %s, %w, %x

Why not adding %m to the party? something like
%m{(expression)(space)(method_name)}
That should not be to hard to idantify and translate to
(expression).method(:method_name)
If there is no expression, we assume the value self

  %m<Math sqrt>                          #=>  #<Method: Math.sqrt>
  %m(foo.bar.baz something)              #=>  #<Method: Baz#something>
  %m:to_s:                               #=>  #<Method: main.to_s()>
  [1, 4, 9, 16, 25].map(& %m"Math sqrt") #=>  [1.0, 2.0, 3.0, 4.0, 5.0]

To enable meta-programming, it can be done the same way than all others %string does it...
with an upper case version.

  x = ::Kernel.gets.chomp
  %M[MyModule #{x}]

In that form, the question I ask myself is if the method_name should be an explicit symbol or not. The above exemple assume it dose not.

Updated by cvss (Kirill Vechera) almost 3 years ago

One more use case - method composition, for example from #18369:

collection.detect(&:first_name>>"Dorian".:==)

Updated by konsolebox (K B) almost 3 years ago

Has && been considered?

Dir["*/*.c"].map(&&File.basename)

It can also adapt to outside usage so &File.basename equals File.method(:basename).

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0