RFC: `with ... yield` replacement


#41

@vlazar Yes, but the idea is also to not have to type too much. Having to do:

html do |&.|
  body do |&.|
  end
end

will quickly become tedious (having to write |&.|). Instead:

html &.do
  body &.do
  end
end

is a bit shorter and IMO feels more natural.


#42

@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.


#43

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.


#44

Also we could probably think of something for named implicit argument RFC: `with ... yield` replacement


#45

Oops, :-) let’s keep brainstorming then.

It seems there is more consensus to move out of with ... yield on to something explicit at the time of using the method.

We need to find a sweat spot syntax for that.


#47

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:

  1. 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.
  2. 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)
  3. Not passing the c parameter and using dot notation does not exemplify crystal’s object orientation approach (visually)

#48

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” :smiley:
  • do: and {:
  • .do and .{
  • do. and {.

Based on

How about trying this?

  1. 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| }
  1. 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

#49

But event with . and if it’s even possible to make it work in Crystal

html do.
  p class: "foo", 2 do.
    "Lorem ipsum"
  end
end

I still would prefer the visual look without any specia chars like

html it
  p class: "foo", 2 it
    "Lorem ipsum"
  end
end

Too bad it is taken by Spec. But some other two-letter words like at or on might make sense too.


#50

I like the & suggestion.

('a'..'z').each& { ::puts upcase } # print 'A', 'B', etc. is what really sold it for me. I currently use the with ... yield in my Kave shard. So I’m assuming this method would become

def api(version)
  yield Kave::ApiDSL.new(version)
end

api("v1")& do
  get "/" do
  end
end

#51

While implementing a DSL for ORM querying, I found this syntax very nice:

Post.query
  .select(:id, :content)
  .join(:author) do
    &.where(id: user.id)
    &.join(:another_table) do
      &.where("id = author.id")
    end
  end

.join call yields an object, so this is equivalent to

Post.query
  .select(:id, :content)
  .join(:author) do |q|
    q.where(id: user.id)
    q.join(:another_table) do |q1|
      q1.where("id = author.id")
    end
  end

I.e.

if having & in the beginning of the call, treat it as the first block argument

, which is equal to the foo &.bar syntax we already have!

html do
  &.p class: "foo", 2 do
    "Lorem ipsum"
  end
end

#52

Apart from syntax it’s about moving responsibility to method caller.

So if you like explicitness you can use

html do |h|
  h.p class: "foo", 2 do |h|
    "Lorem ipsum"
  end
end

Otherwise something implicit to remove |h| and h.. Ideally just as simple as

html do
  p class: "foo", 2 do
    "Lorem ipsum"
  end
end

#53

This change will break all specs.

Spec and Spec2 use with ... yield.

I think it’s not a worth breaking change.


#54

Reviewed the list of ideas in this thread again. If we go with using keyword for this I was thinking that we can’t use in , but it looks like it’s not used now?

So maybe we can in fact replace

App.routes do |c|
  c.get :foo 
  c.post :bar 
end

With

App.routes in
  get :foo 
  post :bar 
end

It’s shorter, but to me it looks similar to this @jhass idea

with App.routes do 
  get :foo 
  post :bar 
end

#55

@vlazar IMHO in is too close to already used out keyword, also the meaning would be pretty vague here - in what?

In general I find adding new keywords for such case a bad idea since blocks have a short form (foo { ... }) too which couldn’t use them, thereby breaking the parity between short and long forms.


#56

Agree, I’ve noticed some clash with out keyword too. It’s just that I feel keywords longer than 2 letters would be to noisy for DSLs (a good example in HTML DSL with lot’s of nested blocks).

From other 2 letter words mentioned here I feel like in kind of makes more sense than others, but that’s just me.

Of course using keyword means no support for { ... } form. Might not be a big deal for DSLs, but I agree, something consistent that can be used both in do ... end and { ... } would be much nicer to have. However proposed solutions that might work for both cases involve using special characters and look noisy.

Even the least noisy (for me) does not look absolutely clean, but can probably support both do ... end and { ... }:

html do.
  p class: "foo", 2 do.
    "Lorem ipsum"
  end
end
html {. p class: "foo" }

I could imagine above being a special case syntax and using . more explicitly for non first block argument like so:

foo do |first, ., third|
  bar # calls bar on second (unnamed) block argument
end

Or maybe even name implicit receiver in case we want to refer to it inside a block:

foo do |first, .second, third|
  bar # still calls bar on second block argument
  p second # use named version of second block argument
end