[ruby-core:122506] [Ruby Feature#21435] Kernel#optional as a conditional #then

Issue #21435 has been reported by Alexander.Senko (Alexander Senko). ---------------------------------------- Feature #21435: Kernel#optional as a conditional #then https://bugs.ruby-lang.org/issues/21435 * Author: Alexander.Senko (Alexander Senko) * Status: Open ---------------------------------------- When chaining, I need sometimes to apply some changes conditionally, like this: ```ruby @record = Record.find(record_id) .then { it.respond_to?(:decorate) ? it.decorate : it } ``` It would be great to DRY it a bit: ```ruby @record = Record.find(record_id) .optional { it.decorate if it.respond_to? :decorate } ``` Reference implementation: ```ruby # 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 ``` The name is discussible. -- https://bugs.ruby-lang.org/

Issue #21435 has been updated by nobu (Nobuyoshi Nakada). 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:
```ruby # 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? ---------------------------------------- Feature #21435: Kernel#optional as a conditional #then https://bugs.ruby-lang.org/issues/21435#change-113730 * Author: Alexander.Senko (Alexander Senko) * Status: Open ---------------------------------------- When chaining, I need sometimes to apply some changes conditionally, like this: ```ruby @record = Record.find(record_id) .then { it.respond_to?(:decorate) ? it.decorate : it } ``` It would be great to DRY it a bit: ```ruby @record = Record.find(record_id) .optional { it.decorate if it.respond_to? :decorate } ``` Reference implementation: ```ruby # 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 ``` The name is discussible. -- https://bugs.ruby-lang.org/

Issue #21435 has been updated by Alexander.Senko (Alexander Senko). 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: ```ruby @record = Record.find(record_id) .optional { it.try :decorate } ``` I just decided not to post Rails-specific code here. ---------------------------------------- Feature #21435: Kernel#optional as a conditional #then https://bugs.ruby-lang.org/issues/21435#change-113732 * Author: Alexander.Senko (Alexander Senko) * Status: Open ---------------------------------------- When chaining, I need sometimes to apply some changes conditionally, like this: ```ruby @record = Record.find(record_id) .then { it.respond_to?(:decorate) ? it.decorate : it } ``` It would be great to DRY it a bit: ```ruby @record = Record.find(record_id) .optional { it.decorate if it.respond_to? :decorate } ``` Reference implementation: ```ruby # 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 ``` The name is discussible. -- https://bugs.ruby-lang.org/

Issue #21435 has been updated by matheusrich (Matheus Richard). 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? ---------------------------------------- Feature #21435: Kernel#optional as a conditional #then https://bugs.ruby-lang.org/issues/21435#change-113733 * Author: Alexander.Senko (Alexander Senko) * Status: Open ---------------------------------------- When chaining, I need sometimes to apply some changes conditionally, like this: ```ruby @record = Record.find(record_id) .then { it.respond_to?(:decorate) ? it.decorate : it } ``` It would be great to DRY it a bit: ```ruby @record = Record.find(record_id) .optional { it.decorate if it.respond_to? :decorate } ``` Reference implementation: ```ruby # 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 ``` The name is discussible. -- https://bugs.ruby-lang.org/

Issue #21435 has been updated by Alexander.Senko (Alexander Senko). 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. ---------------------------------------- Feature #21435: Kernel#optional as a conditional #then https://bugs.ruby-lang.org/issues/21435#change-113734 * Author: Alexander.Senko (Alexander Senko) * Status: Open ---------------------------------------- When chaining, I need sometimes to apply some changes conditionally, like this: ```ruby @record = Record.find(record_id) .then { it.respond_to?(:decorate) ? it.decorate : it } ``` It would be great to DRY it a bit: ```ruby @record = Record.find(record_id) .optional { it.decorate if it.respond_to? :decorate } ``` Reference implementation: ```ruby # 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 ``` The name is discussible. -- https://bugs.ruby-lang.org/

Issue #21435 has been updated by mame (Yusuke Endoh). 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: ```ruby @record = Record.find(record_id) @record = @record.decorate if @record.respond_to?(:decorate) ``` ---------------------------------------- Feature #21435: Kernel#optional as a conditional #then https://bugs.ruby-lang.org/issues/21435#change-113736 * Author: Alexander.Senko (Alexander Senko) * Status: Open ---------------------------------------- When chaining, I need sometimes to apply some changes conditionally, like this: ```ruby @record = Record.find(record_id) .then { it.respond_to?(:decorate) ? it.decorate : it } ``` It would be great to DRY it a bit: ```ruby @record = Record.find(record_id) .optional { it.decorate if it.respond_to? :decorate } ``` Reference implementation: ```ruby # 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 ``` The name is discussible. -- https://bugs.ruby-lang.org/

Issue #21435 has been updated by Alexander.Senko (Alexander Senko). 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:
```ruby @record = Record.find(record_id) @record = @record.decorate if @record.respond_to?(:decorate) ```
Those who prefer iterative logic over method chains never needed `#then` as well. ---------------------------------------- Feature #21435: Kernel#optional as a conditional #then https://bugs.ruby-lang.org/issues/21435#change-113738 * Author: Alexander.Senko (Alexander Senko) * Status: Open ---------------------------------------- When chaining, I need sometimes to apply some changes conditionally, like this: ```ruby @record = Record.find(record_id) .then { it.respond_to?(:decorate) ? it.decorate : it } ``` It would be great to DRY it a bit: ```ruby @record = Record.find(record_id) .optional { it.decorate if it.respond_to? :decorate } ``` Reference implementation: ```ruby # 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 ``` The name is discussible. -- https://bugs.ruby-lang.org/

Issue #21435 has been updated by Alexander.Senko (Alexander Senko). @zverok, what do you think about it? May `#then` get a conditional counterpart? ---------------------------------------- Feature #21435: Kernel#optional as a conditional #then https://bugs.ruby-lang.org/issues/21435#change-113742 * Author: Alexander.Senko (Alexander Senko) * Status: Open ---------------------------------------- ## What When chaining, I need sometimes to apply some changes conditionally, like this: ```ruby @record = Record.find(record_id) .then { it.respond_to?(:decorate) ? it.decorate : it } ``` It would be great to DRY it a bit: ```ruby @record = Record.find(record_id) .optional { it.decorate if it.respond_to? :decorate } ``` Or, even shorter for Rails users: ```ruby @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 ```ruby # 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 ``` -- https://bugs.ruby-lang.org/

Issue #21435 has been updated by zverok (Victor Shepelev). @Alexander.Senko I don't think my opinion weights much here, but here it is, nevertheless. When writing in "chaining" style (which I know is not everybody's favorite), "do this transformation/next step conditionally" is indeed a frequent thing that comes to mind. There are two cases: a) the condition depends on the `self` (the current step of the chain) and b) the condition doesn't depend on it. For the latter, I would've liked (though I don't feel strongly enough to propose it) something like `then_if`: ```ruby def save(data, path, compress: true) data .do_something .do_something_else .to_json .then_if(compress) { Compression.call(it) } # skipped just returning self if condition is false .then { File.write(path, it) } end ``` For a case when the step depends on `self` in that step, I don't have an idea of a good syntax, TBH. Maybe something like this (though purely theoretical, I wouldn't really want to add more lambdas to the code this way, both for "too much punctuation" and performance concerns): ```ruby data .do_something .do_something_else .to_json .then_if(-> { it.size > LIMIT }) { Compression.call(it) } .then { File.write(path, it) } ``` I understand where the idea of `optional` is coming from, but for me, both the name and the usage seem kinda ambiguous. In any case, I rarely miss this option that much, and never had a good idea of a "proper" syntax/API to allow that. ---------------------------------------- Feature #21435: Kernel#optional as a conditional #then https://bugs.ruby-lang.org/issues/21435#change-113759 * Author: Alexander.Senko (Alexander Senko) * Status: Open ---------------------------------------- ## What When chaining, I need sometimes to apply some changes conditionally, like this: ```ruby @record = Record.find(record_id) .then { it.respond_to?(:decorate) ? it.decorate : it } ``` It would be great to DRY it a bit: ```ruby @record = Record.find(record_id) .optional { it.decorate if it.respond_to? :decorate } ``` Or, even shorter for Rails users: ```ruby @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 ```ruby # 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 ``` -- https://bugs.ruby-lang.org/

Issue #21435 has been updated by Alexander.Senko (Alexander Senko). Subject changed from Kernel#optional as a conditional #then to Kernel#then_try as a conditional #then Description updated `optional` -> `then_try` ---------------------------------------- Feature #21435: Kernel#then_try as a conditional #then https://bugs.ruby-lang.org/issues/21435#change-113760 * Author: Alexander.Senko (Alexander Senko) * Status: Open ---------------------------------------- ## What When chaining, I need sometimes to apply some changes conditionally, like this: ```ruby @record = Record.find(record_id) .then { it.respond_to?(:decorate) ? it.decorate : it } ``` It would be great to DRY it a bit: ```ruby @record = Record.find(record_id) .then_try { it.decorate if it.respond_to? :decorate } ``` Or, even shorter for Rails users: ```ruby @record = Record.find(record_id) .then_try { 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: - `then_try` - `optional` - `maybe` ## Reference implementation ```ruby # Yields self to the block and returns the result of the block if it’s # truthy, and self otherwise. def then_try tap do result = yield(self) or next break result end end ``` -- https://bugs.ruby-lang.org/

Issue #21435 has been updated by matz (Yukihiro Matsumoto). Status changed from Open to Rejected From my point of view, it doesn't make the code clearer. Besides that, the core Ruby does not provide even `#try`. Maybe ActiveSupport wants to add this method, but not in the core. Matz. ---------------------------------------- Feature #21435: Kernel#then_try as a conditional #then https://bugs.ruby-lang.org/issues/21435#change-113991 * Author: Alexander.Senko (Alexander Senko) * Status: Rejected ---------------------------------------- ## What When chaining, I need sometimes to apply some changes conditionally, like this: ```ruby @record = Record.find(record_id) .then { it.respond_to?(:decorate) ? it.decorate : it } ``` It would be great to DRY it a bit: ```ruby @record = Record.find(record_id) .then_try { it.decorate if it.respond_to? :decorate } ``` Or, even shorter for Rails users: ```ruby @record = Record.find(record_id) .then_try { 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: - `then_try` - `optional` - `maybe` ## Reference implementation ```ruby # Yields self to the block and returns the result of the block if it’s # truthy, and self otherwise. def then_try tap do result = yield(self) or next break result end end ``` -- https://bugs.ruby-lang.org/
participants (6)
-
Alexander.Senko (Alexander Senko)
-
mame (Yusuke Endoh)
-
matheusrich (Matheus Richard)
-
matz (Yukihiro Matsumoto)
-
nobu (Nobuyoshi Nakada)
-
zverok (Victor Shepelev)