Crystal: Add a way to create a proc by binding a non-captured block to a context

Created on 25 Apr 2017  路  14Comments  路  Source: crystal-lang/crystal

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?

feature discussion lang

Most helpful comment

there's no indiction that foo does 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.

All 14 comments

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:

  • A macro, with support for edge cases, e.g. support for block arguments
  • A new syntax, like capture_with context yield

Main problem resolved:

I want to defer a block execution with a given context

Possible use-cases:

  • Hooks with DSL
  • Lazy configuration (run later, and/or multiple times)
  • _possibly a lot of others_...

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

  1. delay the context association with a block
  2. store this in a proc for later use

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 foo does 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.

Was this page helpful?
0 / 5 - 0 ratings