Chaining & to_proc on symbol

2020-02-10 06:58发布

问题:

It's well known to Rubyist & will call to_proc on a symbol, so

[:a, :b, :c].map(&:to_s)

is equivalent to

[:a, :b, :c].map { |e| e.to_s } # => ["a", "b", "c"]

Say I want to call another method right after to_s, these two implementations will work:

[:a, :b, :c].map { |e| e.to_s.upcase }
[:a, :b, :c].map(&:to_s).map(&:upcase)

My question is, is there a way to chain the & Symbol#to_proc call in one parameter? Something like:

[:a, :b, :c].map(&:to_s:upcase)

Thanks!

回答1:

If you're only doing:

%i[a b c].map { |e| e.to_s.upcase }

then just use the block and get on with more important things. If you're really doing a chain of Enumerable calls and find the blocks too visually noisy:

%i[a b c].map { |e| e.to_s.upcase }.some_chain_of_enumerable_calls...

then you could toss your logic into a lambda to help clean up the appearance:

to_s_upcase = lambda { |e| e.to_s.upcase }
%i[a b c].map(&to_s_upcase).some_chain_of_enumerable_calls...

or throw it in a method and say:

%i[a b c].map(&method(:to_s_upcase)).some_chain_of_enumerable_calls...

Either way, you're giving your little bit of logic a name (which is pretty much all &:symbol is doing for you) to make the code more readable and easier to understand. In the specific case of to_s.upcase, this is all a bit pointless but these approaches are quite useful when the block gets bigger.



回答2:

You will need to define some method in advance, but this will have generality. You can do like this:

class Symbol
  def * other
    ->x{x.send(self).send(other)}
  end
end

[:a, :b, :c].map(&:to_s * :upcase)
[:a, :b, :c].map(&:to_s * :capitalize)
...

I chose * as a method for functional composition.

And if you think you might use a third symbol, you can define like:

class Proc
  def * other
    ->x{call(x).send(other)}
  end
end


回答3:

So just for fun (and to prove that almost anything is possible in ruby if one puts in a bit of effort) we could define a method on Symbol (we'll call it Symbol#chain) to provide this functionality and a little more

class Symbol
  def proc_chain(*args)
    args.inject(self.to_proc) do |memo,meth|
      meth, *passable_args = [meth].flatten
      passable_block = passable_args.pop if passable_args.last.is_a?(Proc)
      Proc.new do |obj|
        memo.call(obj).__send__(meth,*passable_args,&passable_block)
      end
    end
  end
  alias_method :chain, :proc_chain
end

This can then be called like so

[:a, :b, :c].map(&:to_s.chain(:upcase))
#=> ["A","B","C"]
# Or with Arguments & blocks
[1,2,3,4,5].map(&:itself.chain([:to_s,2],:chars,[:map,->(e){ "#{e}!!!!"}]))
#=>  => [["1!!!!"], ["1!!!!", "0!!!!"], ["1!!!!", "1!!!!"],
#        ["1!!!!","0!!!!", "0!!!!"], ["1!!!!", "0!!!!", "1!!!!"]]

Can even be used as a standalone

p = :to_s.chain([:split,'.'])
p.call(123.45)
#=> ["123","45"]
# Or even 
[123.45,76.75].map(&p)
#=> => [["123", "45"], ["76", "75"]]


回答4:

While we're playing with syntax, how about monkey-patching Array with a to_proc method?

class Array
  def to_proc
    return :itself.to_proc if empty?
    ->(obj) { drop(1).to_proc.call(first.to_proc.call(obj)) }
  end
end

["foo", "bar", "baz"].map(&[:capitalize, :swapcase, :chars, ->a{ a.join("-") }])
# => ["f-O-O", "b-A-R", "b-A-Z"]

See it on repl.it: https://repl.it/JS4B/1



回答5:

There is no way to chain using the symbol to proc.

However, you could monkey patch a method to the class you are mapping over that will do both, then call that.

class Symbol
  def to_upcase_str
    self.to_s.upcase
  end
end

[:a, :b, :c].map(&:to_upcase_str)