The with ... yield
feature has some drawbacks that would be good to solve. To illustrate the main challenge, both from the user and from the compiler, let’s see the following code:
foo do
bar
end
It is unclear (without checking the source of foo) where bar can be declared.
This level of uncertainty is an upper bound regarding how much code can be compiled/analyzed in a modular way eventually. Blocks are widely used in crystal code.
When the block is executed without a with ... yield
the body is resolved in the same lexical scope where the block is written by the user. But today, unless the foo
source is analyzed there is no clue how to solve the lookup.
So we are penalizing most of the blocks usages, generating uncertainty for the user and some extra work for the compiler.
The with ... yield
was introduced to allow dsl as in Ruby. They are a powerful tool.
Some people don’t mind having an explicit context like:
App.routes do |c|
c.get :foo
c.post :bar
end
But some do believe that
App.routes do
get :foo
post :bar
end
looks better and is more powerful.
Sadly it has the drawbacks mentioned earlier.
The proposal I want to share is to remove the with ... yield
but to introduce a new syntactic convention for method calls. A method call with a trailing &
would have the semantic of evaluating the block body in the context yielded by the callee method.
Note: Choosing chars for new syntax is always problematic, the core of the proposal is a new syntax for a method call that won’t clash with the rest of the language. Which is the actual syntax for this is a different aspect of the actual mechanism.
From an implementation detail this could be done by a local transformation.
So
App.routes& do
get :foo
post :bar
end
could be translated to
App.routes do |c|
c.get :foo
c.post :bar
end
And be compiled as usual.
I do think that changing the current context / removing the explicit receiver is a key part of a good dsl.
Some additional considerations:
- the user can decide whether to use implicit or explicit contexts for each invocation.
- arguments work with the syntax.
html& do
p& class: "foo", 2 do
"Lorem ipsum"
end
end
-
top level methods. Depending on the implementation they might not be callable, but I don’t think it’s terrible given the common usage. Ultimately a
::method
could disambiguate. -
additional arguments in the block. Initially I wouldn’t allow them. I haven’t seen DSLs with arguments in blocks unless that argument is the context.
-
do
/end
vs{ ... }
. Since the new syntax applies on the method name both block syntax works. -
choosing another syntax/character. Instead of
&
I prefered:
, but it clashes with named arguments too much (in the previous html example, parsing would require lookahead forp: class: "foo"
). -
Using
&
as a suffix can be seen related to the&.proc
notation also which refer to the first argument of the block.