Crystal: [rfc] Import Object#yield_self from Ruby 2.5

Created on 12 Oct 2017  路  23Comments  路  Source: crystal-lang/crystal

Ruby 2.5 decide to introduce Object#yield_self (see here). This method yields self to the block and then returns its result. The implementation is very simple:

class Object
  def yield_self
    yield self
  end
end

Should we import this method into Crystal? What do you think?

Most helpful comment

It's similar to .tap in behavior and intent, expect it returns the block's value (not the tapped value).

I don't see much usage. Naming is verbose, and assigning to a value feels better:

list = long.chained.computation(12).foo(1, 2, 3)
result = (list.sum * list.product).to_s.size

All 23 comments

It's basically Object#try which also works for Nil with a descriptive yet still uninviting long name.

I don't really see the usecase..?

@RX14 Yes, I'd like to ask Ruby committer about usecase.

Not that I defend it, but could be used to create a variable, without creating a variable. To help keep calculations inline:

result = long.chained.computation(12).foo(1, 2, 3).yield_self {|list| list.sum * list.product }.to_s.size

RethinkDB (a database whose query language is mostly functional) has a similar operation, called do: https://rethinkdb.com/api/ruby/do/.

It is like a map, but for the entire thing.

It's similar to .tap in behavior and intent, expect it returns the block's value (not the tapped value).

I don't see much usage. Naming is verbose, and assigning to a value feels better:

list = long.chained.computation(12).foo(1, 2, 3)
result = (list.sum * list.product).to_s.size

@RX14 Why do you close this issue? (I can guess but...) If you could explain this, you should do it, or if you couldn't explain this, you shouldn't close this.

The consensus was that it wasn't really very useful. If anyone has a counterexample to discuss we can always reopen.

yield_self is for those that want a "pipe operator" in Ruby: http://mlomnicki.com/yield-self-in-ruby-25/

Like others here, I don't think it looks nice, nor I think it's readable. And since we have try (and you will probably never want to have a nil in a chain), it can work as good as yield_self.

I found very useful usecase of Object#yield_self! See this simple fizz buzz example:

def fizz?(n)
  n % 3 == 0
end

def buzz?(n)
  n % 5 == 0
end

(1..100).each do |i|
  if fizz?(i) && buzz?(i)
    puts :FizzBuzz
  elsif fizz?(i)
    puts :Fizz
  elsif buzz?(i)
    puts :Buzz
  else
    puts i
  end
end

When we have Object#yield_self, we can rewrite this:

# `def fizz?` and `def buzz` is skipped

(1..100).each do |i|
  case i
  when .yield_self { |i| fizz?(i) && buzz?(i) }
    puts :FizzBuzz
  when .yield_self { |i| fizz?(i) }
    puts :Fizz
  when .yield_self { |i| buzz?(i) }
    puts :Buzz
  else
    puts i
  end
end

In other words, Object#yield_self provides a potential to call any method in when condition against case value.

However, yield_self is tooooooooo long to type. So, I suggest another name Object#let (derived from Kotlin). What do you think?

@RX14 Please re-open this. I believe this feature makes Crystal more useful and we can discuss about this more.

But why would you want to do that? That's uglier than the if version by far. If we just want a way to use arbitrary booleans in case then we should discuss that without resorting to hacks.

Real world example, src/compiler/crystal/tools/doc/highlighter.cr:

      case token.type
      when :NEWLINE
        io.puts

      # ...snip...

      when :IDENT
        if last_is_def
          last_is_def = false
          highlight token, "m", io
        else
          case token.value
          when :def, :if, :else, :elsif, :end,
               :class, :module, :include, :extend,
               :while, :until, :do, :yield, :return, :unless, :next, :break, :begin,
               :lib, :fun, :type, :struct, :union, :enum, :macro, :out, :require,
               :case, :when, :then, :of, :abstract, :rescue, :ensure, :is_a?,
               :alias, :pointerof, :sizeof, :instance_sizeof, :as, :typeof, :for, :in,
               :undef, :with, :self, :super, :private, :protected, "new"
            highlight token, "k", io
          when :true, :false, :nil
            highlight token, "n", io
          else
            io << token
          end
        end
      when :"+", :"-", :"*", :"/", :"=", :"==", :"<", :"<=", :">", :">=", :"!", :"!=", :"=~", :"!~", :"&", :"|", :"^", :"~", :"**", :">>", :"<<", :"%", :"[]", :"[]?", :"[]=", :"<=>", :"==="
        highlight token, "o", io
      when :"}"
        if break_on_rcurly
          break
        else
          io << token
        end
      else
        io << token
      end

when condition for keywords and operators are too long, so I want to refactor this with such constants:

KEYWORDS = Set{
  :def, :if, :else, :elsif, :end,
  :class, :module, :include, :extend,
  :while, :until, :do, :yield, :return, :unless, :next, :break, :begin,
  :lib, :fun, :type, :struct, :union, :enum, :macro, :out, :require,
  :case, :when, :then, :of, :abstract, :rescue, :ensure, :is_a?,
  :alias, :pointerof, :sizeof, :instance_sizeof, :as, :typeof, :for, :in,
  :undef, :with, :self, :super, :private, :protected, "new"
}

OPERATORS = Set{
  :"+", :"-", :"*", :"/", :"=", :"==", :"<", :"<=", :">", :">=", :"!",
  :"!=", :"=~", :"!~", :"&", :"|", :"^", :"~", :"**", :">>", :"<<",
  :"%", :"[]", :"[]?", :"[]=", :"<=>", :"==="
}

But I can't use these constants in when condition because Set#=== is not specialized currently.

When there is Object#let, this code can be rewritten:

      case token.type
      when :NEWLINE
        io.puts

      # ...snip...

      when :IDENT
        if last_is_def
          last_is_def = false
          highlight token, "m", io
        else
          case token.value
          when .let { |v| KEYWORDS.includes? v }
            highlight token, "k", io
          when :true, :false, :nil
            highlight token, "n", io
          else
            io << token
          end
        end
      when .let { |t| OPERATORS.includes? t }
        highlight token, "o", io
      when :"}"
        if break_on_rcurly
          break
        else
          io << token
        end
      else
        io << token
      end

Of course Object#let is not needed if #5269 is merged. However it is actual thing that Object#let is the most generic way to call any method against case value.

@RX14 Please imagine. Why do you hate this issue?

I think it makes sense. It would be nice to see which is faster, but I guess Set#includes? is faster than a huge when (I think this was concluded in a recent issue).

Benchmark is here and result is this:

old 358.88  (  2.79ms) (卤 2.53%)       fastest
new 337.75  (  2.96ms) (卤 1.87%)  1.06脳 slower

Object#let version is slow, but I feel it is a bit, also fast. It looks no problem to me.

/cc @asterite


And the most important point I think:

it is actual thing that Object#let is the most generic way to call any method against case value.

It feels like an ugly hack, like there should be a concrete, real, syntax change for supporting this, not hacking it into the stdlib. Perhaps this could work:

```cr
case foo
when { func(foo) }
end

@RX14 that would require some lookahead in the parser to disambiguate from tuples I think. Besides the .foo { } is using already existing constructs.

Maybe Object#bind, Object#apply or Object#itself(&block) are short enough ?

The let looks weird to me. expr.let { |var| S } instead of let var = expr in S everything seems twisted.

Another proposal which doesn't require a change:

if func(foo)
  # do something
end

Well, {func(foo)} is a valid tuple so clearly my syntax idea isn't sound. However, I maintain that if the only point of adding this method is for case then we'd be better off fixing the root of the problem - case. Can't think up of a good syntax though.

Object#into is possible candidate. (or maybe Object#in, but it is too short.) I think #apply or #itself is too long.

IMHO Object#yield_self isn't bad, but perhaps #tap! could be used?

#tap! does not make sense. I think ! means mutable or danger, but this method is not mutable normally and safe. Additionally #tap is used with mutable method sometimes.

Replacing with if is good sometimes, on the other hand, it is not so good sometimes. I think highlighter.cr is such an example. When normal when value condition and other method call is mixed, this method is really useful.

And another benefit of this method is to call any method in method chaining. Any Crystal users dislike this because they are genius, so they can think the best variable name every time. However I can't do it every time. Naming is important, but writing executable program is more important, so I like this.

Re Object#let, FYI there's a ruby gem with exactly that name, see rubygems object-let.

Disclaimer: I'm the author. Not that there's much code in there :)

I abuse #try for that too. Most time it serves well, but sometimes feels like a hack: some_method.not_nil!.try....

Was this page helpful?
0 / 5 - 0 ratings