Project

General

Profile

Feature #15557

A new class that stores a condition and the previous receiver

Added by sawa (Tsuyoshi Sawada) 3 months ago. Updated 3 months ago.

Status:
Open
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:91220]

Description

I often see code like this:

foo = default_definition
foo = some_method(foo) if some_condition(foo)
foo = another_method(foo) if another_condition(foo)
...

It would be nice if we can write this as a method chain. Since we now have the method then, I thought it would be a nice fit to introduce a method called when, such that putting it right in front of then would execute the then method as ordinarily only when the condition is satisfied, and returns the previous receiver otherwise so that the code above can be rewritten as:

foo =
default_definition
.when{|foo| some_condition(foo)}
.then{|foo| some_method(foo)}
.when{|foo| another_condition(foo)}
.then{|foo| another_method(foo)}

This proposal is also a generalization of what I intended to cover by https://bugs.ruby-lang.org/issues/13807. That is,

a.some_condition ? a : b

would rewritten as:

a.when(&:some_condition).then{b}

The proposal can be implemented by introducing a class called Condition, which stores a condition and the previous receiver, and works with then in a particular way.

class Object
  def when
    Condition.new(self, yield(self))
  end
end

class Condition
  def initialize default, condition
    @default, @condition = default, condition
  end

  def then
    @condition ? yield(@default) : @default
  end
end

And additionally, if we introduce a negated method unless (or else) as follows:

class Object
  def unless
    Condition.new(self, !yield(self))
  end
end

then we can use that for purposes such as validation of a variable as follows:

bar =
gets
.unless{|bar| some_validation(bar)}
.then{raise "The input is bad."}
.unless{|bar| another_validation(bar)}
.then{raise "The input is bad in another way."}

Related issues

Related to Ruby trunk - Feature #13807: A method to filter the receiver against some conditionClosedActions

History

#1

Updated by sawa (Tsuyoshi Sawada) 3 months ago

  • Description updated (diff)
#2

Updated by sawa (Tsuyoshi Sawada) 3 months ago

  • Description updated (diff)

Updated by nobu (Nobuyoshi Nakada) 3 months ago

What about:

foo = default_definition
      .when(->(foo) {some_condition(foo)}) {|foo| some_method(foo)}
      .when(->(foo) {another_condition(foo)}) {|foo| another_method(foo)}
#4

Updated by nagachika (Tomoyuki Chikanaga) 3 months ago

  • Related to Feature #13807: A method to filter the receiver against some condition added
#5

Updated by sawa (Tsuyoshi Sawada) 3 months ago

  • Description updated (diff)

Updated by zverok (Victor Shepelev) 3 months ago

I played a bit with idea similar to nobu (Nobuyoshi Nakada)'s, with this syntax:

then_if(condition) { action }
then_if(-> { condition }) { action }

Explanations:

  • boolean vs callable: sometimes, static conditions are useful, sligtly realistic example:
  def fetch(url, parse: false)
    get(url).body
      .then_if(parse) { |res| JSON.parse(res) }
      # or, simpler:
      .then_if(parse, &JSON.:parse)
  end
  • name: "it is like then, but conditional"

I experimented a bit with syntax like sawa (Tsuyoshi Sawada)'s initial proposal, and believe that it could work too (but method's name should probably be if ;)), with the same adjustment that it can receive pre-calculated condition, too. The problem with it, though, is the introduction of the new abstraction/"mode" for the sake of nice syntax, e.g. something.if(&:cond?) on itself doesn't produce an easily explainable/reusable object.

The problem with nobu (Nobuyoshi Nakada)'s one, though, that it is pretty rarely seen in core Ruby to pass callable object instead of just a block (though, it exists, like nobu (Nobuyoshi Nakada) pointed in another ticket, for example in Enumerator::new)

Updated by shevegen (Robert A. Heiler) 3 months ago

Normally I love sawa's suggestions. :)

However had, in this case, I am a little bit biased, which I may explain.

First, I should start here by agreeing with sawa in regards to the use case noted on top is quite common -
I use it myself a lot, but I see it in other people's ruby code as well:

foo = default_definition
foo = some_method(foo) if some_condition(foo)
foo = another_method(foo) if another_condition(foo)

Or perhaps in a simpler variant, more often this:

foo = default_definition
foo = some_method(foo) if some_condition
foo = another_method(foo) if another_condition

These can be method calls too:

foo = new_colour if use_colours?

I think the latter can be quite common if people wish to determine
what users may want to use; e. g. from a commandline interface,
like if you start a project with commandline flags --disable-colours
or any other flag. So yes, I think this can be quite common.

sawa gave an alternative proposal:

.when{|foo| some_condition(foo)}
.then{|foo| some_method(foo)}
.when{|foo| another_condition(foo)}
.then{|foo| another_method(foo)}

Now I should say that I personally actually prefer the "oldschool" variant
since, to me personally, the intent is more clear. Another minor concern I
have is that .when and .then are not so easy to distinguish visually; when
is also used in case/when menu interfaces, which I love (so I am not that
enthusiastic about using more "when" in other places in ruby, to be
honest).

Which variant to prefer is of course heavily up to the individual person
at hand but in my opinion I think using mixed .when and .then is not as
easy or straightforward than the other variant.

Nobu suggested:

.when(->(foo) {some_condition(foo)}) {|foo| some_method(foo)}

Here I can not comment much on it since I feel that in my own projects
the "->" always felt odd. Combining it with .when would make the code
even more odd (to me). Obviously the milage of other people may differ
here, so this is again a personal preference.

Aside from syntactic considerations, I also feel that the variant with
.when(->(foo) {another_condition(foo)}) {|foo| another_method(foo)} is
harder to break up in the head than the oldschool if/else variant checks.

sawa also suggested:

a.some_condition ? a : b

would rewritten as:

a.when(&:some_condition).then{b}

which I think is not ideal either, because it is longer. I myself avoid the ternary
operator, though, because it always takes my brain a little longer than e. g.
just an if; and a return (if we can avoid using an else altogether, or even better,
to simply use a conditional method call such as one that uses a boolean
return, if we can avoid if/else branching altogether, which can lead to even
simpler code layouts). But this is again up to one's personal preference, and
since I am biased I am admittedly not trying to find good alternative point of
views. :)

Do note that I am not at all against sawa's suggestion or statement that "would be nice if
we can write this as a method chain". I think that part is fine; avoiding if/else branches
can be good in some cases. I am just somewhat concerned in regards to the syntax-verbosity
and that it may lead to more complicated code - the intent in the proposal otherwise is
perfectly fine. This is of course just my personal opinion. :)

zverok just wrote this as I was about to finish my comment ... :P

for the sake of nice syntax, e.g. something.if(&:cond?) on itself doesn't produce an
easily explainable/reusable object.

Here we have to ask whether the alternative syntax proposals are better or really
are a "nice syntax". I have my doubts. :)

Also note that:

something.if
something.else
something.if(&:cond?) 
something.else(&:cond?) 

I consider actually significantly worse syntax. :D

Updated by nobu (Nobuyoshi Nakada) 3 months ago

zverok (Victor Shepelev) wrote:

The problem with nobu (Nobuyoshi Nakada)'s one, though, that it is pretty rarely seen in core Ruby to pass callable object instead of just a block (though, it exists, like nobu (Nobuyoshi Nakada) pointed in another ticket, for example in Enumerator::new)

Enumerable#grep.

Updated by zverok (Victor Shepelev) 3 months ago

Enumerable#grep

Well, it is not "it accepts callable", it is a part of "it accepts any pattern (including Proc)", and passing Proc into grep makes sense only in "polymorphic pattern situation" (we have a pattern variable, which can be Proc, or Regexp, or Range)... I mean, nobody probably writes just

something.grep(->(x) { x.odd? })

...because they write

something.select(&:odd?)

My point was, there are not much core APIs that make people used to some_method(-> { some proc })

Updated by nobu (Nobuyoshi Nakada) 3 months ago

zverok (Victor Shepelev) wrote:

Enumerable#grep

Well, it is not "it accepts callable", it is a part of "it accepts any pattern (including Proc)", and passing Proc into grep makes sense only in "polymorphic pattern situation" (we have a pattern variable, which can be Proc, or Regexp, or Range)... I mean, nobody probably writes just

something.grep(->(x) { x.odd? })

I meant an example of condition and processing by one method.

something.grep(->(x) { x.odd? }) {|x| do_odd_thing(x)}

...because they write

something.select(&:odd?)

Enumerable#select with a block needs chained methods.

Also available in: Atom PDF