Feature #17471
opensend_if method for improved conditional chaining
Description
Background¶
Method chaining is very important to many Ruby users, since everything in Ruby is an object.
It also allows easier functional programming, because it implements a pipeline where each step can happen without mutation.
Conditional chaining allows an even more declarative style of programming. Right now, it is possible to conditionally chain methods to a degree but in some cases it is a bit verbose.
Proposal¶
I propose that a send_if
method is added, which works roughly like this:
# Internal condition
puts 'If you give me a number larger than 5, I will double it. I will subtract 1 in any case.'
number = gets.chomp.to_i
# An implementation without send_if
puts (number > 5 ? number.send(:*, 2) : number).send(:-, 1)
# Implementation with send_if [1]
puts number.send_if(:*, 2) {|obj| obj > 5}.send(:-, 1)
# External condition
puts 'Do you want a loud Merry Christmas? (y or I take it as a no)'
answer = gets.chomp
# An implementation without send_if
puts %w(Merry Christmas).send(:map, &->(e) {answer == 'y' ? e.upcase : e}).join(' ')
# Implementation with send_if [2]
puts %w(Merry Christmas).send_if(:map, proc: :upcase ) { answer == 'y' }.join(' ')
Implementation¶
Here is a Ruby implementation (obviously, everything is released under the same license terms as Ruby itself):
class Object
def send_if(method, *args, proc: nil)
yield(self) ? self.send(method, *args, &proc) : self
end
end
This implementation works as intended with both examples I posted above.
Evaluation¶
I don't believe send_if
brings significant performance penalties, compared to the alternatives.
I am not 100% satisfied with my implementation in terms of usability, for two reasons:
- I did not find any stdlib methods which are consistent with the function signature I've specified. More specifically, I don't like the named
proc:
parameter I used, but I couldn't think of a better alternative. Please, tasukete! - Ruby does not support multiple blocks, which would be required for my ideal implementation (short of [3], see later):
puts %w(Merry Christmas).send_if(:map, &:upcase) { answer == 'y' }.join(' ')
Discussion¶
I know for sure there are more skilled Rubyists than myself here who can come up with nicer alternatives to my send_if
examples, but I think send_if
would be nice to have because:
- The
*_if
family of methods is a staple of the stdlib (e.g.receive_if
,delete_if
,keep_if
, etc.) - In some cases, it decreases the amount of code needed
I know my examples could be written without ever using send
but send
makes it possible to use any Ruby method (rather than write specific methods like map_if
, etc.).
In the future, some syntactic sugar could be built so that method chaining is even more fluid, without any need for send
. An example using an .?{}
operator I just made up:
# Syntax-level conditional chaining [3]
puts %w(Merry Christmas).?{answer == 'y'}map(&:upcase).join(' ')
Of course, {answer == 'y'}
would be a block and this would be equivalent to my example above [2], but without any need for a send
method (since this operator would apply to all methods).
If someone is interested, I can make a separate proposal for this operator, but perhaps it's asking too much :)
I'd be happy to discover more elegant solutions and critiques!
Merry Christmas to everybody and thanks for reading!
Updated by osyo (manga osyo) almost 4 years ago
hi.
How about using #tap
+ break
?
# Proposal
puts number.send_if(:*, 2) {|obj| obj > 5}.send(:-, 1)
puts %w(Merry Christmas).send_if(:map, proc: :upcase ) { answer == 'y' }.join(' ')
# tap + break
puts number.tap { break _1 * 2 if _1 > 5 }.send(:-, 1)
puts %w(Merry Christmas).tap { break _1.map(&:upcase) if answer == 'y' }.join(' ')
FYI : Proposal of [Feature #15829] Object#then_if(condition){}
Updated by ozu (Fabio Pesari) almost 4 years ago
osyo (manga osyo) wrote in #note-1:
puts number.tap { break _1 * 2 if _1 > 5 }.send(:-, 1)
puts %w(Merry Christmas).tap { break _1.map(&:upcase) if answer == 'y' }.join(' ')
Hello and thanks for sharing this Ruby idiom, wouldn't have thought of it myself and I like it a lot!
I guess there are a couple of reasons I would still prefer an explicit send_if
method:
- Because the
*_if
family of method already exists and their usage is predictable - Because the control flow with
send_if
would be a bit more explicit. I guess less skilled Rubyists would take a while to figure out tap + break, because it's a control flow disruption
FYI : Proposal of [Feature #15829] Object#then_if(condition){}
I've seen that proposal and get the general sense of it, however I don't like the condition being the argument because it's inconsistent with keep_if
and delete_if
from e.g. Array
.
Now, #15557 would be an alternative to my proposal. If I understood it correctly, that way you could write my send_if
code as:
# This time I'll too use numbered parameters :)
# Proposal
puts number.send_if(:*, 2) { _1 > 5 }.send(:-, 1)
puts %w(Merry Christmas).send_if(:map, proc: :upcase ) { answer == 'y' }.join(' ')
# With #15557
puts number.when { _1 > 5 }.then { _1 * 2 }.send(:-, 1)
puts %w(Merry Christmas).when { answer == 'y' }.then { _1.map(&:upcase) }.join(' ')
The only thing which I still prefer about send_if
is that it requires no mutable state internally, however given it's a single assignment I would gladly accept it for the sake of readability.
I don't think the two proposals are incompatible though, they could coexist and they are both predictable. send_if
would sometimes result in more concise code but at the moment, given that Ruby doesn't support multiple blocks, I believe what @sawa (Tsuyoshi Sawada) proposed implements the same style of programming in a more elegant way.