Feature #18276
closed`Proc#bind_call(obj)` same as `obj.instance_exec(..., &proc_obj)`
Added by ko1 (Koichi Sasada) almost 3 years ago. Updated over 2 years ago.
Description
Proc#bind_call(obj)
same as obj.instance_exec(..., &proc_obj)
proc_obj = proc{|params...| ...}
obj.instance_exec(params..., &proc_obj)
is frequent pattern.
$ gem-codesearch 'instance_exec.+\&' | wc -l
9558
How about to introduce new method Proc#bind_call
?
class Proc
def bind_call obj, *args
obj.instance_exec(*args, &self)
end
end
pr = ->{ p self }
pr.bind_call("hello") #=> "hello"
pr.bind_call(nil) #=> nil
It is similar to UnboundMethod#bind_call
.
My motivation;
I want to solve shareable Proc's issue https://bugs.ruby-lang.org/issues/18243 and one idea is to prohibit Proc#call
for shareable Proc's, but allow obj.instance_exec(&pr)
. To make shortcut, I want to introduce Proc#bind_call
.
UnboundProc
is another idea, but I'm not sure it is good idea...
Anyway, we found that there are many usage of instance_exec(&proc_obj)
, so Proc#bind_call
is useful not for Ractors.
Updated by jeremyevans0 (Jeremy Evans) almost 3 years ago
I'm in favor of adding this method, and would like to see it support the following:
pr = ->(*a, **kw, &block) do
# ...
block.call(something)
end
pr.bind_call(obj, arg, kw: nil) do |something|
# ...
end
This would allow you to get the equivalent of instance_exec
, but with passing a block to the proc being instance execed, which is not currently possible.
Updated by jhawthorn (John Hawthorn) almost 3 years ago
In Rails we use obj.instance_exec(&proc_obj)
in a few places. One of the downsides instance_exec
has is that it creates a singleton class for obj
, which isn't friendly to method caches or JITs. Proc#bind_call
would be very useful to us if it behaved similarly but did not create a singleton class.
Updated by Eregon (Benoit Daloze) almost 3 years ago
jhawthorn (John Hawthorn) wrote in #note-2:
In Rails we use
obj.instance_exec(&proc_obj)
in a few places. One of the downsidesinstance_exec
has is that it creates a singleton class forobj
, which isn't friendly to method caches or JITs.Proc#bind_call
would be very useful to us if it behaved similarly but did not create a singleton class.
I think instance_exec
doesn't create a singleton class, only if needed by e.g. Object.new.instance_exec { def foo; end }
(at least on TruffleRuby, maybe it depends how eagerly the cref/default definee is computed by the Ruby implementation).
I think the same applies to Proc#bind_call
(-> { def foo; end }.bind_call(Object.new)
).
I'm not against such a method, but IMHO this alone is not solving #18243 in a reasonable way.
If an object is made shareable, every object reachable from it should be frozen or shareable, and magically ignoring the Proc's self
is conceptually ugly and I believe very confusing for many.
Maybe Ractor.make_shareable(someProc)
should return a Proc with a special receiver (e.g., Qundef), which simply can't be called via .call
on any Ractor and can only be called via Proc#bind_call
.
Then at least that shareable Proc wouldn't refer to any unshared object (which would violate the docs and expected semantics of Ractor.make_shareable
)
Updated by Eregon (Benoit Daloze) almost 3 years ago
I don't like the idea to alter the semantics of Proc methods just for Ractor though (also it costs extra checks on every Proc#call !).
The best and cleanest solution is IMHO to raise for Ractor.make_shareable(someProc)
if the Proc self
is not shareable.
See https://bugs.ruby-lang.org/issues/18243#note-5 for more details on that idea.
Then Proc#bind_call
is simply not needed for Ractor.
I don't mind adding Proc#bind_call
for other purposes though.
Changing the self
is typically best avoided except for some cases in DSLs, as it breaks what methods the block can call in its lexical context.
Updated by Eregon (Benoit Daloze) almost 3 years ago
- Related to Bug #18243: Ractor.make_shareable does not freeze the receiver of a Proc but allows accessing ivars of it added
Updated by Dan0042 (Daniel DeLorme) almost 3 years ago
Why not proc.bind(obj).call
? It seems a more "proper" API, more composable. You can bind once and then call multiple times. Maybe Ractor.make_shareable(proc.bind(nil))
. I understand the performance benefit of bind_call
but in #15955, UnboundMethod#bind_call was introduced as an optimization for hot spots, to be used by "only some fundamental libraries".
Updated by Eregon (Benoit Daloze) almost 3 years ago
Dan0042 (Daniel DeLorme) wrote in #note-6:
Why not
proc.bind(obj).call
? It seems a more "proper" API, more composable. You can bind once and then call multiple times. MaybeRactor.make_shareable(proc.bind(nil))
. I understand the performance benefit ofbind_call
but in #15955, UnboundMethod#bind_call was introduced as an optimization for hot spots, to be used by "only some fundamental libraries".
:+1: I think that's useful. I thought to the name Proc#with_self(nil)
in #18243 but #bind
is much better.
Ractor.make_shareable(proc.bind(nil))
is a clean solution, I like it.
I think we don't even need Proc#bind_call
then, or only as a replacement for instance_exec(&proc)
.
Updated by byroot (Jean Boussier) almost 3 years ago
only as a replacement for instance_exec(&proc).
Assuming I correctly understand how it would work, then yes it would be great to have it to replace lots of costly instance_exec
.
Updated by Eregon (Benoit Daloze) almost 3 years ago
byroot (Jean Boussier) wrote in #note-8:
Assuming I correctly understand how it would work, then yes it would be great to have it to replace lots of costly
instance_exec
.
I don't think it would change anything performance-wise. The only thing is it's possible to pass a block to the called proc that way (https://bugs.ruby-lang.org/issues/18276#note-1, https://bugs.ruby-lang.org/issues/18276#note-3).
Updated by Eregon (Benoit Daloze) almost 3 years ago
Ah, maybe Koichi meant that bind_call
doesn't change the default definee like instance_exec
does?
i.e., does
class C
-> {
def foo
end
}.bind_call(Object.new)
end
define foo
on that object's singleton class, or as an instance method of class C?
I'd assume on that object's singleton class like instance_exec
, but I guess it's not the only possibility.
Updated by Dan0042 (Daniel DeLorme) almost 3 years ago
Eregon (Benoit Daloze) wrote in #note-10:
I'd assume on that object's singleton class like
instance_exec
, but I guess it's not the only possibility.
I would assume the same thing; it would be pretty strange if this defined a foo
method on class C.
But it's interesting how instance_eval/instance_exec automatically creates a singleton_class, I wasn't aware of that before.
p ObjectSpace.each_object(Class).count #=> 363
Object.new.instance_eval{ }
p ObjectSpace.each_object(Class).count #=> 364
Object.new.instance_exec{ }
p ObjectSpace.each_object(Class).count #=> 365
It looks like the singleton_class is eagerly created just in case the eval'd block contains a def
. But it shouldn't be too hard to fix that to lazily create the singleton_class when needed.
Updated by ko1 (Koichi Sasada) almost 3 years ago
Thank you for discussion.
My assumption is not same as instance_exec/eval
, only replacing the self
.
So the description was wrong.
Updated by ko1 (Koichi Sasada) almost 3 years ago
Dan0042 (Daniel DeLorme) wrote in #note-6:
Why not
proc.bind(obj).call
? It seems a more "proper" API, more composable. You can bind once and then call multiple times. MaybeRactor.make_shareable(proc.bind(nil))
. I understand the performance benefit ofbind_call
but in #15955, UnboundMethod#bind_call was introduced as an optimization for hot spots, to be used by "only some fundamental libraries".
Proc#bind(obj)
returns new Proc or modify the Proc (mutate the Proc)?
Updated by Eregon (Benoit Daloze) almost 3 years ago
ko1 (Koichi Sasada) wrote in #note-13:
Proc#bind(obj)
returns new Proc or modify the Proc (mutate the Proc)?
Returns a new Proc, mutation would be very bad (similar to changing from proc to lambda semantics).
My assumption is not same as instance_exec/eval, only replacing the self.
I think we should optimize #instance_exec in CRuby so it only creates the singleton class lazily, like on TruffleRuby.
It seems several people care about that.
Not changing the default definee seems confusing, I think .bind.call/.bind_call should behave like instance_exec in that regard (instance_exec can already be surprising, let's not make an extra variant of it with subtle changes).
Updated by ko1 (Koichi Sasada) over 2 years ago
- Status changed from Open to Rejected
Ok, I close this ticket.