This does not work: (https://carc.in/#/r/1xbh)
def capture(ctx)
->{ with ctx yield } # can't use `yield` inside a proc literal or captured block
end
struct A
property var = 0
end
a_ctx = A.new
proc = capture a_ctx do
itself.var = 42
end
proc.call
puts a_ctx.var
Is there a way to achieve this kind of thing ?
Then is there any reason this has been blocked?
Thanks to a hint from @ziprandom, I got it working with a simple macro:
module Block
def self.yield_with_context(context)
with context yield
end
macro capture_with_context(context, &block)
-> { Block.yield_with_context {{context}} {{block}} }
end
end
proc = Block.capture_with_context 10 do
puts itself + 32
end
proc.call # => 42
Live at: https://carc.in/#/r/23du (with an example with a class instance)
@ziprandom The trick you showed on gitter/irc doesn't work, because the block was called without context, and yours wasn't generic for any block
I think this should be in the stdlib, because it could be a common use to defer a block execution on a specific context.
We could introduce it via:
capture_with context yieldMain problem resolved:
I want to defer a block execution with a given context
Possible use-cases:
@bew your right, my example didn't work.
I thought it did because of my poor choice of example context :) . Here the explicitly breaking version:
https://play.crystal-lang.org/#/r/23lu
question: should this work? or can't this work at all and a compile time error should be thrown?
can't this work at all and a compile time error should be thrown?
Maybe I don't understand your question, but It _does_ work with the macro way.
Should this work?
IMO yes, I don't know what was your use-case, but my idea was to defer the call to a block with a context. e.g. to be able to call it later, and/or multiple times, on different context (the context internals can be dynamic)
@bew your example is totally fine. It uses macros to actually rewrite the block. thats the way I also do it now in my project. my (not-working) example didn't use macros. E.g. when you try to do it this way
def capture_block_not_really
->(context : Int32) { with context yield }
end
you get an error at compile time:
Error in line 5: instantiating 'capture_block_not_really()'
in line 2: can't use `yield` inside a proc literal or captured block
Make sure to read the whole docs section about blocks and procs,
including "Capturing blocks" and "Block forwarding":
http://crystal-lang.org/docs/syntax_and_semantics/blocks_and_procs.html
I'm just wondering if my broken example actually runs into the same issue only bypassing the compiler warning mechanism somehow. If that is the case this should be fixed to not even compile ...
Oh I understand the question now, yes you're bypassing the compiler, or put another way, the compiler doesn't detect the error:
# By writing `&block ...` you already capture the block
def make_proc(&block: Int32 -> Int32)
# Here, typeof(block) # => Proc(Int32, Int32)
Proc(Context, Int32, Int32).new do |context, args|
# Now you're trying to pass a captured block (the receiver cannot be changed)
# to a `with .. yield` through a function call, bypassing the compiler detection
yield_with_args(context, args, &block)
end
end
def yield_with_args(context, args)
# here yield refers to a proc, not a block, the compiler should raise !
with context yield args
end
So no, it shound't work, and yes there should be a compilation error like:
Error: can't use `with ... yield` with an already captured proc
The phrasing isn't good, but you get the idea!
It seems you somehow solved this.
Indeed I solved my original issue, thanks for closing @asterite. About the possible bug I explain in my previous message, about with ... yield notation should errors when used with a captured block, do I open a new issue for this?
@bew I don't understand what you are saying. But when I saw the code you ended up implementing, it's definitely not the same as what you wanted to achieve in the same place.
I'm saying that when using with .. yield notation with a proc instead of a block, I would expect it to error, not be simply bypassed, e.g:
def call_with_yield
with 21 yield
end
proc = ->{ puts 42 }
# In this case, the `with .. yield` notation is useless, and bypassed. I think should errors.
call_with_yield &proc # => "hello"
About how my initial issue and the implementation, all I wanted was to be able to:
My use case was a little DSL for lazy configuration, where the configuration could be "replayed" on different context when I need it, something like:
store_config do
setup_thing1
setup_thing2
end
exec_config context1
exec_config context2
To do that I needed to bind the config block to a specific context (which will act as a proxy to the actual context I want to configure), so I can call the config proc later on demand... does that make sense?
Oh, right, the above snippet shouldn't compile.
I wouldn't improve the behaviour of with ... yield. In any case, I'd remove it from the language.
There's no way to delay the context association with a block. That works in dynamic languages. In Crystal it's impossible to implement. And there are ways to accomplish this without using procs or with ... yield, just use an "interface" and pass it to a method, or to a block.
In any case, I'd remove it from the language.
Disallowing what it allows to do? or by introducing a different way to do it?
Completely removing the feature. It serves little purpose. You can already do:
def foo
yield 42
end
foo do |ctx|
ctx.something
end
instead of:
def foo
with 42 yield
end
foo do
something
end
The second code is confusing because it's hard to know what something refers to, and there's no indiction that foo does something magical.
I understand this provides DSLs, but DSLs don't solve problem, they just add a small syntax improvement, which here is minor and confusing.
there's no indiction that
foodoes something magical.
There's no indication either when we use macros to do fancy stuff.
Also, I think this is is a bad example, because the name of the method foo means nothing, and I think that there is a big responsibility on the method name you give, so users can understand what it does. Also there is the documentation which should be used to explain exactly what it does, and how to use this or that method.
For example the macro getter: you don't have any warning that it's a macro, and that it should be used as getter var = 1 or getter var or getter var : Int32. All of this resides in the documentation, and the naming is good enough to make you think it will define a getter in a way..
I understand this provides DSLs, but DSLs don't solve problem, they just add a small syntax improvement, which here is minor and confusing.
For me DSL are very important, and being able to have them with the clean syntax of Crystal is a must.
DSLs are not directly related to with ... yield I think, as we can already do wonderful things with macros and how we call methods in Crystal (without (), with positional/named args, etc..).
But I think it helps reducing redundant things like this one:
store_config do |configurator|
configurator.setup_thing1
configurator.setup_thing2
configurator.setup_thing3
configurator.setup_thing4
configurator.setup_thing5
end
Here configurator is maybe too self explanatory, but it serves my purpose well: When you have several things to configure like this, it's just cluttered to have configurator everywhere. It could be named ctx or c but then it loose its meaning, and it's purpose (allow the user to understand what he does.. in a way).
DSLs using with .. yield are to be used by end-users of a given library/framework, I think we can agree to say it's a bad practice to do otherwise and use it everywhere in your huge codebase (the same way it's a bad practice to overuse macros). But it's very nice to be able to expose simple things uncluttered with how it works internally.
Most helpful comment
There's no indication either when we use macros to do fancy stuff.
Also, I think this is is a bad example, because the name of the method
foomeans nothing, and I think that there is a big responsibility on the method name you give, so users can understand what it does. Also there is the documentation which should be used to explain exactly what it does, and how to use this or that method.For example the macro
getter: you don't have any warning that it's a macro, and that it should be used asgetter var = 1orgetter varorgetter var : Int32. All of this resides in the documentation, and the naming is good enough to make you think it will define a getter in a way..For me DSL are very important, and being able to have them with the clean syntax of Crystal is a must.
DSLs are not directly related to
with ... yieldI think, as we can already do wonderful things with macros and how we call methods in Crystal (without(), with positional/named args, etc..).But I think it helps reducing redundant things like this one:
Here
configuratoris maybe too self explanatory, but it serves my purpose well: When you have several things to configure like this, it's just cluttered to haveconfiguratoreverywhere. It could be namedctxorcbut then it loose its meaning, and it's purpose (allow the user to understand what he does.. in a way).DSLs using
with .. yieldare to be used by end-users of a given library/framework, I think we can agree to say it's a bad practice to do otherwise and use it everywhere in your huge codebase (the same way it's a bad practice to overuse macros). But it's very nice to be able to expose simple things uncluttered with how it works internally.