Project

General

Profile

Actions

Feature #21435

open

Kernel#optional as a conditional #then

Added by Alexander.Senko (Alexander Senko) 4 days ago. Updated 2 days ago.

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

Description

What

When chaining, I need sometimes to apply some changes conditionally, like this:

@record = Record.find(record_id)
  .then { it.respond_to?(:decorate) ? it.decorate : it }

It would be great to DRY it a bit:

@record = Record.find(record_id)
  .optional { it.decorate if it.respond_to? :decorate }

Or, even shorter for Rails users:

@record = Record.find(record_id)
  .optional { it.try :decorate }

Why

The intent is to make it visible at a glance that a statement may affect the result, but not necessarily does so. Without the proposed method, one needs to read and parse the whole block to know that.

It should help to read longer processing chains, for those who prefer chains and #then to plain old iterative approach.

Naming

It is discussible. I have just two ideas yet:

  • optional
  • maybe

Reference implementation

# Yields self to the block and returns the result of the block if it’s
# truthy, and self otherwise.
def optional
  tap do
    result = yield(self) or next

    break result
  end
end

Updated by nobu (Nobuyoshi Nakada) 4 days ago

I agree that the pattern sometimes appears.
But the name optional feels kind of ambiguous or too generic, to me.

Alexander.Senko (Alexander Senko) wrote:

Reference implementation:

# Yields self to the block and returns the result of the block if it’s
# truthy, and self otherwise.
def optional
  yield(self) or self
end

Regarding respond_to?, IIRC, isn't ActiveSupport's try based on it?

Updated by Alexander.Senko (Alexander Senko) 4 days ago

nobu (Nobuyoshi Nakada) wrote in #note-1:

Regarding respond_to?, IIRC, isn't ActiveSupport's try based on it?

Yes, it would be even simpler with Rails:

@record = Record.find(record_id)
  .optional { it.try :decorate }

I just decided not to post Rails-specific code here.

Updated by matheusrich (Matheus Richard) 4 days ago

I'm sorry, I don't understand the use case, nor how it DRY things up.

The given example shaves off 1 character. What is optional doing? What is "optional" referring to?

Updated by Alexander.Senko (Alexander Senko) 4 days ago

matheusrich (Matheus Richard) wrote in #note-3:

I'm sorry, I don't understand the use case, nor how it DRY things up.

The given example shaves off 1 character. What is optional doing? What is "optional" referring to?

The idea is a) to reduce cognitive complexity by removing one trivial branch of if/else statement or an ugly ternary operator, and b) to highlight explicitly that the statement is conditional and may leave the result intact in some cases.

The method may improve readability of longer processing chains when some of the operations are applied conditionally. Without it, we have to repeat that same else branch for every conditional processing in a chain -- that looks a bit cumbersome and not very DRY for me.

I'm not a native speaker, sorry. IMO, optional indicates that execution is not guaranteed and depends on a condition. Maybe it could be called maybe instead.

Updated by mame (Yusuke Endoh) 3 days ago

To be honest, when I see a code fragment like .optional { it.decorate if it.respond_to? :decorate }, I couldn't understand the intended behavior at all. Personally, I feel that this actually increases cognitive complexity rather than reducing it.

It's not just that the method name optional feels inappropriate. I think the existence of a method with such tricky behavior itself adds to cognitive complexity.

In terms of cognitive complexity, I find it much easier to understand when the logic is written out explicitly, even if it's more verbose, like this:

@record = Record.find(record_id)
@record = @record.decorate if @record.respond_to?(:decorate)

Updated by Alexander.Senko (Alexander Senko) 3 days ago · Edited

mame (Yusuke Endoh) wrote in #note-5:

In terms of cognitive complexity, I find it much easier to understand when the logic is written out explicitly, even if it's more verbose, like this:

@record = Record.find(record_id)
@record = @record.decorate if @record.respond_to?(:decorate)

Those who prefer iterative logic over method chains never need #then as well.

Actions #7

Updated by Alexander.Senko (Alexander Senko) 2 days ago

  • Description updated (diff)
Actions #8

Updated by Alexander.Senko (Alexander Senko) 2 days ago

  • Description updated (diff)

Updated by Alexander.Senko (Alexander Senko) 2 days ago

@zverok (Victor Shepelev), what do you think about it? May #then get a conditional counterpart?

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like1Like0Like0Like0Like0