RFC: `with ... yield` replacement

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

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

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

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.

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)

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
1 Like

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.

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

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

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

This change will break all specs.

Spec and Spec2 use with ... yield.

I think it’s not a worth breaking change.

1 Like

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

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

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

Old thread, but here’s my two cents. Can something like this make sense?
(alert: quick and dirty, proof of concept, incomplete and probably buggy but somewhat functioning code):

private macro _fix_rcvrless(rcvr,type,node) {% puts node %} {%
  if    type.is_a?(Call) %}{% type = type.receiver.resolve %}{%
  elsif type.is_a?(Path) %}{% type = type.resolve %}{%
  end %}{%
  if    node.is_a? Call                %} {% if !node.receiver && type.methods.find{|m|m.name==node.name} %} {{rcvr}}.{{node}} {% # the actual thing -- all the rest is just to get here
                                            elsif node.block %}{% begin %}{%if node.receiver %}{{node.receiver}}.{%end%}{{ node.name }} do |{{*node.block.args}}| _fix_rcvrless({{rcvr}},{{type}},{{node.block.body}}) end {%end%}{% # FIXME: block.args
                                            else %} {{node}} {%
                                            end %} {%
  elsif node.is_a? Assign              %} {{node.target}} = _fix_rcvrless({{rcvr}},{{type}},{{node.value}}) {%
  elsif node.is_a? And                 %} _fix_rcvrless({{rcvr}},{{type}},{{node.left}}) && _fix_rcvrless({{rcvr}},{{type}},{{node.right}}) {%
  elsif node.is_a? Or                  %} _fix_rcvrless({{rcvr}},{{type}},{{node.left}}) || _fix_rcvrless({{rcvr}},{{type}},{{node.right}}) {%
  elsif node.is_a? BinaryOp            %} {{ node }} {% #TODO?
  elsif node.is_a? Block               %} do |{{ *node.args }}| _fix_rcvrless({{rcvr}},{{type}},{{node.body}},true) end {% #FIXME -- args could be empty, splat_index, how to know if it's do/end or {} and does it matter?
  elsif node.is_a? Case                %} {{ node }} {% #TODO
  elsif node.is_a? Expressions         %} {% for e in node.expressions %}_fix_rcvrless(rcvr, e); {% end %} {% # newlines?
  elsif node.is_a? If                  %} if _fix_rcvrless({{rcvr}},{{type}},{{node.cond}}) then _fix_rcvrless({{rcvr}},{{type}},{{node.then}}) else _fix_rcvrless({{rcvr}},{{type}},{{node.else}}) end {% # newlines?
  elsif node.is_a? MultiAssign         %} {{ node }} {% #TODO
  elsif node.is_a? Path                %} {{ node }} {% #TODO
  elsif node.is_a? ProcLiteral         %} {{ node }} {% #TODO
  elsif node.is_a? ProcNotation        %} {{ node }} {% #TODO
  elsif node.is_a? RangeLiteral        %} {{ node }} {% #TODO
  elsif node.is_a? StringInterpolation %} {{ node }} {% #TODO
  elsif node.is_a? UnaryExpression     %} {{ node }} {% #TODO
  elsif node.is_a? When                %} {{ node }} {% #TODO
  elsif node.is_a? While               %} {{ node }} {% #TODO
  else %}{{ node }}{%
  end %}
end
macro _in(context,&block)
  {{ context }} do |ctx|
    _fix_rcvrless(ctx,{{context}},{{block.body}})
  end
end

begin # example
  module MyDslContext
  extend self # because no `TypeNode#class_methods`, I can't `def self.xyz`
    def something
      yield self
    end
    def dsl_method1(arg)
      puts {{@def.name.stringify}} + " " + arg.to_s
    end
  end

  def a_method
    _in MyDslContext.something do
      dsl_method1 :blah
    end
  end
  a_method

  class SomeClass
    def yielder; yield 123 end
    def another_method
      _in MyDslContext.something do
        yielder do |num|
          dsl_method1 num
        end
      end
    end
  end
  SomeClass.new.another_method
end # example

The funky placement of macro delimiters is to suppress extra newlines in the output (I didn’t yet notice there’s \ line continuation when I wrote this).
On the good side, it apparently allows to comment out a faulty macro line, at least in some cases.

Again, note that this is incomplete and there may be issues with this approach down the road.

As far as I can tell, I have to assume the implicit receiver will come from a yield self because that allows to use the context argument as the type of the receiver. I haven’t yet found a way to know in macro code what the actual yielded type is. And it could be useful to yield an instance of some other type instead of self.


I’d like to hear opinions on this approach to “code rewriting” and traversing the parse tree in general.
Don’t worry, I myself question its sanity.

I particularly hate “reconstructing” the parts I don’t want to modify, ideally I’d like to do {% node.something = something_else %} and then {{ node }}. And be able to define and call a possibly recursive “macro subroutine”. BTW I noticed I can def inside {% %} but then cannot call it, and the same goes for a proc.

I would like to say, whatever approach you choose, try to avoid cryptic symbols in favor of more clarity, even if that means few more characters!
I came to crystal because of its naturally readable syntax and I don’t like to see too many &.<>%^| and the like.
Thanks

4 Likes