-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[rfc] Import Object#yield_self from Ruby 2.5 #5110
Comments
It's basically |
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:
RethinkDB (a database whose query language is mostly functional) has a similar operation, called It is like a map, but for the entire thing. |
It's similar to 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. |
Like others here, I don't think it looks nice, nor I think it's readable. And since we have |
I found very useful usecase of 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 # `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, However, @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 |
Real world example, 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
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 there is 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 @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 |
Benchmark is here and result is this:
/cc @asterite And the most important point I think:
|
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: case foo
when { func(foo) }
end |
@RX14 that would require some lookahead in the parser to disambiguate from tuples I think. Besides the Maybe The |
Another proposal which doesn't require a change: if func(foo)
# do something
end |
Well, |
|
IMHO |
|
Replacing with 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 Disclaimer: I'm the author. Not that there's much code in there :) |
I abuse |
It seems that, other than a caveat with Therefore, I propose two things: 1. I encourage the team to rethink the decision against 2. If the first proposal is not accepted (or even if it is), the Here are two real-world use cases for Suppose we have a web request handler that enqueues a background job and notifies the user of the result. def enqueue_job
# enqueues a background job
end
def notify_user
# sends some JSON to the user's browser
end
def handle_request
# ...
enqueue_job.then do |result|
case result
when :accepted
notify_user(:accepted)
when :rejected
notify_user(:rejected)
else
log(result)
notify_user(:error)
end
end
# ...
end I find this construction in the functional-programming style nicer than storing A similar example, where def handle_request(attrs)
log_request attrs.redact_private_data
.yield_self do |redacted_attrs|
{
name: redacted_attrs["name"],
email: redacted_attrs["email"],
ip: request.remote_ip,
admin: current_user.admin?
}
end
save_to_database(attrs)
end |
Disclaimer: The following is just my personal opinion on code style. Looking at the examples by @elebow, I don't buy that this is a good way to express the code's intention. The first example looks much better readable to me when you assign the value in the def handle_request
# ...
case result = enqueue_job
when :accepted
notify_user(:accepted)
when :rejected
notify_user(:rejected)
else
log(result)
notify_user(:error)
end
end
# ...
end For the second example, without looking into the exact semantics of the methods being called, just considering the structural shape, I think it does a bad job explaining what's going on. It's a chain of methods being called, with the last call receiving a block that constructs a hash. It's not very obvious that this hash is actually the return value of the entire call chain. Consider that alternative implementation instead. It does exactly the same thing, with a lot less visual clutter and much clearer in expressing how the log request data is created. def handle_request(attrs)
redacted_attrs = attrs.redact_private_data
log_request({
name: redacted_attrs["name"],
email: redacted_attrs["email"],
ip: request.remote_ip,
admin: current_user.admin?
})
save_to_database(attrs)
end I think the major point is that I don't agree with this sentiment:
IMO it's not worth going lengths just to avoid introducing additional local variables in the method namespace. Introducing an additional scope just for the sake of that doesn't pay out. Maybe it could be an idea to think about finding a middle ground for limiting the scope of local variables in some situations. For example, a local variable declared in a conditional expression (such as |
@straight-shoota, I agree your suggested code is neater than my examples. Still, I think the style has merit in some situations. See these other real-world examples:
As for a language change regarding the scope of local variables, I agree that would be detrimental. I would argue only for adding the methods to (mentioning #1388 (comment) to link a related discussion that is not yet referenced in this thread). |
I'm pretty sure that most of the examples you posted would actually be easier to read if they were written as independent expressions instead of as a chain with
Sure, |
I had to search, but I finally found one case where it may be nice (maybe): def key
@key ||= begin
cipher = OpenSSL::Cipher.new(DEFAULT_CIPHER)
cipher.random_key.hexstring
end
end
# self_yield allows to skip the begin/end block:
def key
@key ||= OpenSSL::Cipher.new(DEFAULT_CIPHER).yield_self do |cipher|
cipher.random_key.hexstring
end
end |
I still we should do it like Ruby and add a Implementing that method is trivial! And it would solve all these scenarios:
|
@ysbaddaden I don't quite understand. Why would you use either of those forms instead of just a call chain? def key
@key ||= OpenSSL::Cipher.new(DEFAULT_CIPHER).random_key.hexstring
end |
@straight-shoota 🤦 ... Allright, let's imagine there is some other def key
@key ||= generate_key
end
private def generate_key
# ...
end @asterite As said above |
I agree, #then seems super idiomatic and explains the operation (and matches ruby, which is pretty normal for crystal) In terms of examples, the reasons I love .then/.try is to build out composable conditionals like such: https://thoughtbot.com/blog/using-yieldself-for-composable-activerecord-relations |
Ruby 2.5 decide to introduce
Object#yield_self
(see here). This method yieldsself
to the block and then returns its result. The implementation is very simple:Should we import this method into Crystal? What do you think?
The text was updated successfully, but these errors were encountered: