Agree. I was more concerned not about using some particular word, but removing noise. Also it is already taken by Spec, so we shouldn’t use it.
The with App.routes do looks really nice to me, also I think it can cause confusion, at least when seeing it first time and mostly because of this particular word with and it’s meaning in other languages.
OTOH it might actually be a quick thing to get accustomed to. Also finding other word for this case might be tricky.
Reading all of the responses persuades me that it would be better to leave it as is. Almost any solution makes the code uglier. I think it’s OK to have such an implicit feature (with yield), because most of the time it’s used with DSL, and DSLs tend to be well-documented.
I like the idea of |&|, it could even be possible to use it for not-the-first argument if the user of the dsl ever want that:
foo do |one, &, three|
bar one, three
end
The only drawback I see would be that we loose the name of the argument along the way, but as we already don’t have it in this case maybe it’s not a problem
setup_routes = ->(self, global_prefix) do
get "#{global_prefix}/foo"
post "#{global_prefix}/bar"
end
App1.routes("/app1") &setup_routes
App2.routes("/app2") &setup_routes
The problem with with expr do is that the arguments of expr will need to have parenthesis.
So I would prefer to leave expr be top called method.
@asterite If we go with & it should be on method name to keep the parenthesis optional. Even if that is a lookahead in the parser since that is only in the method name, while parsing identifiers. It’s in a well defined place.
We need to support arguments without noise as much as possible to have a good DSL. Something as similar as:
div class: "foo" do
p "bar"
end
The with for any object will be expr.tap ... with whatever syntax could be.
I haven’t used DSL where the yielded grammar block has many arguments. I would not mind leaving that out of scope and using arg.tap ... to mimic that.
Back to syntax. I agree using a new keyword looks cleaner. But we will lose the chance to use expr { }. I can accept that. It will also allow better error reporting. Let’s say we use doit, then if the block wants or the body yields other than 1 argument, then, there is a compile time error.
I also thought that does might play good as a keyword.
Let’s try the discourse polling to collect the opinions.
@vlazar Yes, the intention is that foo { &. something } looks similar to what we have right now. In fact I wouldn’t mind foo { &.something } to work too. Or we could change it to be before the block like you propose (making &. a keyword of sort), so foo &.{ something }. I think I like your proposal the best, since:
foo &.something
can be seen as a shortcut to:
foo &.{ something }
with that &.{ ... } meaning "use the first block argument implicitly in the entire block.
Now, to use &. you can do:
foo &.something
but if you have arguments you have to write it like:
foo 1, 2, &.something
Notice the comma before &.. That’s because & is a block argument, it’s not a block, and arguments come separated by comma. So overloading &. might bring a lot of confusion, but I’m not sure.
With do it’ll be:
App.routes &. do
get :foo
post :bar
end
App.routes 1, 2 &. do
...
end
App.routes(1, 2) &. do
...
end
But not this:
App.routes 1, 2, &.do
...
end
because &.do expands to block_arg.do… unless we consider that as an exception, but exceptions are bad.
So I still don’t know which one I prefer. Probably &. after do/{ is better because it can’t be confused with &.before the block.
Actually, I’m thinking that having &.do and &.{ as special constructs might be the best because, as I said above, they can be seen as applying &. to the entire block. Now I don’t mind having &.do not mean block_arg.do.
With clean DSL argument in mind above everything else… The doit or does are cleaner than using special characters but reads a bit weird and are long words.
I remember the feeling when I was learning Ruby first when everything was looking so nice, but some things like do ... end were just too noticeable in Ruby after less noisy { ... } in other languages. Get used to it eventually, but it took me quite some time.
@asterite Right, it’s tradeoff. If clean DSL is the most important thing, then making something work for all cases wouldn’t do as it’s more generic and noisy. Unless we consider to add both of course, but probably not since we are trying to simplify things, not make them more complex.
Another thing with do |&.| is that it could allow the possibility to make any block argument be the implicit one… but I’m not sure how useful would that be, specially because it doesn’t have a name. But it’s definitely more “powerful” than the other approaches.
I think it’s akin to a shorthand notation of sorts, and should be optional. It’s consistent with how block forwarding works here: "1,3,3".split(",").map &.to_i, and the more explicit version "1,3,3".split(",").map{|x|x.to_i}
But also I feel
App.routes& do
get :foo
post :bar
end
is less explicit/intuitive and more confusing for several reasons:
The whereabouts of get and post (or any method inside that block) could be coming from anywhere and are unknown. Unless the developer knows in advance what methods App.routes has, but now the developer needs to sift through another class in their brain to remember if that method exists for that class, instead of just looking at the passed blocked and seeing the .methodname.
When a developer glances over the code, they might think one of the methods are not bound to c's object (basically back to point 1)
Not passing the c parameter and using dot notation does not exemplify crystal’s object orientation approach (visually)
So I was shuffling different chars again. Mostly : and . as less noisy for cleaner DSL.
Putting only : or better . near do or { from either side but without spaces does look better to me than & or &.
:do and :{ - can even have a name like “mustached block”
do: and {:
.do and .{
do. and {.
Based on
How about trying this?
Reserve : or . for marking block argument as implicit receiver. It shouldn’t necessarily be the first argument.
foo do |., second, third|
end
foo { |first, ., third| }
# maybe even this
# second is a named argument now, but also an implicit receiver
foo { |first, .second, third| }
Special case syntax just to drop extra spaces and || in case of single (first) argument.
I like that . goes not directly after a method call, but inside a block, closer to where we actually use an implicit receiver.
ary.map do.
"#{to_s} : #{to_s(16)}"
end
ary.map {. "#{to_s} : #{to_s(16)}" }
ary.map &.to_s # seems to expand nicely, & basically just adds {} around
ary.map .to_s # or maybe replace by this instead, but break Ruby-like parsing?
Real example where noise quickly becomes annoying.
html do.
p class: "foo", 2 do.
"Lorem ipsum"
end
end
Compare it with some of the previous ideas:
html& do
p& class: "foo", 2 do
"Lorem ipsum"
end
end
html &.do
p class: "foo", 2 &.do
"Lorem ipsum"
end
end
html do &.
p class: "foo", 2 do &.
"Lorem ipsum"
end
end