-
-
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
Compiler: add implicit block arguments (_1, _2, etc.) #9216
Conversation
I love it. And agree on the usefulness for shorter code and for try is really nice. That’s primarily what I’d use it for. Easier to write and read in these cases |
This is similar in Elixir also: https://elixir-lang.org/getting-started/modules-and-functions.html I don’t find it confusing using I love this feature. You know it because we talked several times about it 😉. But in any case I would leave the feature and discussion post 1.0 |
I could/would definitely use this right now, as it would make allot of my code shorter and easier to read. |
Note: "you" here doesn't mean the author, just anyone Ah yes. "Pass a block that accepts no arguments and gets the 2nd from the 1st." That's how one reads this code. Easy discount on WTFs/minute. And don't try to tell me you'll get used to it. Well, even if you will, the vast majority won't. But I don't believe you will either. This will be a mental interrupt every single time. Because you couldn't be bothered to declare the args, every time someone reads that code will have to stumble on it. Do we deserve that? Is it OK to keep going in the direction where code can be read only by its author even if it's not logically convoluted? I personally wouldn't want that, and I definitely don't trust people to not abuse it (i.e. put it into any expression > 20 chars, where the distance between "cool, it accepts no args" and "wait, what's first?" is already too much) -- sadly Crystal would be particularly exposed to this compared to other languages due to the association with Ruby where there's just no common sense of restraint on one-liners. |
What's the semantic for nested blocks and block short syntax? files.each { Log.debug { _1 } } That won't work, and it'll log nothing, or the arg to Log.debug, or fail at compile time if nothing is given to Log.debug' block (I don't remember). But what about: files.each { Log.debug &.entry(_1) } Using the dsl syntax demoed in another log pr iirc, will that log each file in |
Yes, I think we should rather think about how to improve the pass a method as block syntax. Maybe we can allow something like this just in a block shorthand? I think this can solve @oprypin's arguments somewhat, first of all enforcing a short distance and and second it doesn't make you mentally enter into block parse mode by introducing a new "short hand block" parse mode. Not a syntax proposal, just trying to show that the examples that motivate this can be equally solved with that: # instead of
[1, 2, 3].each { puts _1 }
# allow
[1, 2, 3].each &:puts
[1, 2, 3].each &:puts(_1)
files = ["foo.cr", "bar.cr"]
# instead of
files.map { File.exists?(_1) }
# allow
files.map &:File.exists?
files.map &:File.exists?(_1)
# instead of
%w(World Computer).each { puts "Hello #{_1}!" }
# allow
%w(World Computer).each &puts("Hello #{_1}!")
# instead of
Hash(String, Array(Int32)).new { _1[_2] = [0] }
# allow
Hash(String, Array(Int32)).new &.[_2]=([0]) Some more from the forum post. Not arguing that the proposal makes them strictly better than the current expanded form, just to demonstrate the problem space that can be attacked by it. # instead of
[1, 2, 3].map { _1 + 3 }
# allow
[1, 2, 3].map &.+(3)
# instead of
(1..9).each_slice(3).map { _1 + _2 + _3 }
# allow
(1..9).each_slice(3).map &.+(_2 + _3) # of course that should just be &.sum
# instead of
[{name: 'foo'}, {name: 'bar'}].map { _1[:name] }
# allow
[{name: 'foo'}, {name: 'bar'}].map &.[:name]
user = User.first?
# instead of
user.try { ChargeCard.call(prince_in-cents: 500, user: _1 }
# allow
user.try &:ChargeCard.call(prince_in-cents: 500, user: _1)
# instead of
password.value.try do
copy_and_encrypt_password _1, to: encrypted_password
end
# allow
password.value.try &:copy_and_encrypt_password(to: encrypted_password)
# above is going to far probably and should be
password.value.try &:copy_and_encrypt_password(_1, to: encrypted_password) So to formalize:
So much for the base. Possible extensions
Please let's focus on the semantics of this proposal first. Any signs chosen here are explicitly not part of the proposal and just used for demonstration purposes! I would prefer to keep the discussion focused on semantics and in case those semantics would be a feature beneficial to the language, we can then discuss the best syntax for this. |
I feel this feature takes us back to the cryptic Perl-land with all those magic What I'd be reeeeally excited about though, is Elixir-like syntax allowing me to do thing like |
// note that you can do this already: But yes, that, or #9197, seems fine to me, as these don't exhibit the biggest problem I see with this proposal, which is that the syntax for a block with 0 args is reused for a block with any (not immediately visible) number of args. |
@oprypin true indeed, what I've missed was the |
Well, that's exactly what this current proposal gives you, then 😄 ❓ |
@Sija it seems this PR is what you want but with a different syntax? |
@oprypin I get what you are saying here. BUT, I personally think this is easier to read in the examples @asterite posted. Elixir has some similar syntax and I don't see it abused much and is generally only used for short method lines. Let's be honest too, people use short meaningless variable names with blocks already. For example: Benchmark.ips do |x|
x.report("short sleep") { sleep 0.01 }
x.report("shorter sleep") { sleep 0.001 }
end What is With all that said, if this is not added it is fine. It is just syntactc sugar that I think has proven to be valuable in places like Elixir and is generally not abused, but I get the argument against it 👍 |
Also know that I first implemented this with &1, & 2, etc., so that syntax is definitely possible. What @jhass suggests, or doing it almost like how elixir does it, by surrounding the block with &(...) might also be a better idea so that you know the replacement is a bit more scoped. |
I like the idea of wrapping it so it is scoped! And I personally think |
OK but see the last part of my message.
Yes but at least you see upfront how many of the variables there are (as opposed to seeming like there are none) |
Yea maybe this proposal could go somewhere if it doesn't reuse the syntax for block with 0 args to mean block with any number of args. |
@oprypin You mean this?
Most elixir programmers came from Ruby so if anything this shows that it can work. especially if scoped with |
For me, |
Most code is read many (10x) more times than it's written. Often by you. At 2am. Six months later. For this reason, I'm a big fan of naming things even if it makes typing it a bit longer. |
I generally agree with well named methods, argument, etc. But some cases there is no better name. Like in the e.g. title.value.try { slugify(&1) } |
For one argument blocks add the keyword ‘it’
(maybe in addition to _1 &1 $1 or #1) Update: never mind I just saw the other pull request. great work, ‘it’ should be merged ;) |
Note: this PR exists because I wanted to see how hard was it to implement it. It's not decided yet whether this is something that we want... but I enjoy coding these things :-) . Also with this people can try it out themselves.
This is exactly like in Ruby. I chose
_1
,_2
, etc. instead of&1
,&2
, etc. because&
means the start of a block or a block arguments:so it would be a bit confusing to reuse
&
to also mean "block argument":I also think
_1
is visually less noisy than&1
. And maybe_1
is a bit easier to type (no need to move the right hand to the middle of the keyboard).Finally, it's the same as in Ruby so Rubyists will feel familiar with this syntax.
Examples
Do I like it?
I think for cases like the
File.exists?
above where it's obvious that each block argument will be a file, and the block is pretty small, that it actually improves readability.Also with
try
when you need to do something other than invoke a method on the yielded object:Note how the two latter ways of writing it introduce a lot of noise (the block syntax, the variable name). The first way is pretty "zen" in my mind.
I also think it finishes generalizing the block syntax. Right now we have the explicit block syntax:
We have the
&.
shorthand for when you want to invoke a method on the first and only block argument:But no syntax exists for the general case when you want to do something with the block arguments without needing to explicitly mention them. Other languages have
it
(Kotlin, just for the first argument), Ruby now has_1
and_2
, and Elixir has the general&(exp(&1, &2))
expansion syntax.Breaking change
This is a tiny breaking change because if you had a variable named
_1
, well, now it's no longer a variable. But I think those cases shouldn't exist... I hope! And fixing those occurrences is easy.