Feature #7548

Load and Require Callbacks

Added by Thomas Sawyer over 1 year ago. Updated about 1 year ago.

[ruby-core:50806]
Status:Open
Priority:Normal
Assignee:Yukihiro Matsumoto
Category:core
Target version:next minor

Description

=begin
Should #load and #require have callbacks? e.g.

def required(path)
...
end

def loaded(path, wrap)
...
end

On occasion I have wanted to do load monitoring to track down a bug. This would have made it easier.

Are there any other good use cases?
=end

History

#1 Updated by Anonymous over 1 year ago

Is there a particular benefit of this over the current setup of require?
require() calls are not async so I don't really see the benefit of
adding callbacks to them.

You mentioned you wanted to use it for monitoring, perhaps you can
further explain that to clarify what you're trying to get at.

Yorick

#2 Updated by Thomas Sawyer over 1 year ago

=begin
There are have been times when I wanted to see what libraries were being loaded and in what order. Usually because of some esoteric bug, and trying to isolate what libraries are involved. In such a case I've had to manually override load and require, something like:

class << Kernel
alias :_require, :require
def require(args)
$stderr.puts "require: %s" % [args.inspect]
_require(
args)
end

alias :_load, :load
def load(*args)
  $stderr.puts "load: %s" % [args.inspect]
  _load(*args)
end

end

module Kernel
def require(args)
Kernel.require(
args)
end
def load(args)
Kernel.load(
args)
end
end

Notice we have to do both Kernel instance and singleton methods to be sure we catch all of them. These days we might also need to add (({#require_relative})) to that.

So the thought then is, if there were callbacks, it would be easier to do, simply:

def required(*args)
$stderr.puts "require: %s" % [args.inspect]
end

def loaded(*args)
$stderr.puts "load: %s" % [args.inspect]
end

Actually considering this a bit more, it would probably be even better just to have one callback method that passed options to designate which kind of loading took place. e.g.

def loaded(file, opts)
opts[:load] => true means #load was used/
false means #require was used

opts[:relative] => true means #require_relative was used

opts[:wrap] => the wrap argument for #load.

end

=end

#3 Updated by Thomas Sawyer over 1 year ago

=begin
Some additional thoughts on this. It occurs to me that #prepend would make this a ((little)) easier now. One could do:

module LoadCallbacks
def load(args)
super(
args)
$stderr.puts "load: %s" % [args.inspect]
end

def require(*args)
  super(*args)
  $stderr.puts "require: %s" % [args.inspect]
end

def require_relative(*args)
  super(*args)
  $stderr.puts "require: %s" % [args.inspect]
end

end

module Kernel
prepend LoadCallbacks
class << self
prepend LoadCallbacks
end
end

That's a little more elegant, but still feels a bit clunky.

I think what would really make the difference, and what would make this callback feature request effectively pointless, is if Ruby had a single method through which all loading was routed. So (({#load})), (({#require})) and (({#require_relative})) would all call this one method passing in options (i.e. named parameters) to tell it which kind of loading is to happen. That would be nice b/c it would allow us a single target method to see what was going on, instead of the current ((six)) methods!
=end

#4 Updated by Boris Stitnicky over 1 year ago

It seems that you are shaping and changing your suggestion with each new post,
so it is a bit hard to react. But the way I see it, your suggestions belong
somewhere to Std-lib 'debug'. Perhaps it might be possible to change stdlib
'debug' to support cherrypicking like ActiveSupport, so you could call
require 'debug/kernel/load_callbacks' if you want those callbacks, but not the
DEBUGGER itself. I am against adding this directly to the core.

#5 Updated by Thomas Sawyer over 1 year ago

It seems that you are shaping and changing your suggestion with each new post,
so it is a bit hard to react.

Sorry. It's a process and discussion sort of request, which is why I presented it first with a question.

But the way I see it, your suggestions belong somewhere to Std-lib 'debug'.
Perhaps it might be possible to change stdlib 'debug' to support
cherrypicking like ActiveSupport,

You mean "cherrypicking like Facets" right? ;)

so you could call require 'debug/kernel/load_callbacks' if you want those callbacks,
but not the DEBUGGER itself. I am against adding this directly to the core.

I think that's very reasonable. Ultimately I like my last suggestion, the one about having a central method that handles loading. That strikes me as a more solid design for Ruby in general. So I think I will make a more concrete proposal for that in another ticket. But short of that, then I agree. Having a standard "cherrypickable" debugging library for this would be the nicest approach.

#6 Updated by Thomas Sawyer over 1 year ago

=begin
Ran into a little snag with implementation of this as a simple Ruby library. It is not possible to override (({#requirerelative})) to add the callback b/c it is lexically scoped (is that the right term for this?). In other words, (({#requirerelative})) uses the (({FILE})) in which it is called. But any override ends up with the (({FILE})) in which the override is defined instead.

Is there a way around this? If not, then the only way to do this properly/completely is via core code.
=end

#7 Updated by Charlie Somerville over 1 year ago

=begin
You can use (({Kernel#caller})) to obtain the filename of the caller
=end

#8 Updated by Thomas Sawyer over 1 year ago

=begin
@charliesome That doesn't quite fix the problem.

# foo/requirerelative.rb
alias
method :requirerelativewithoutcallback, :requirerelative

def requirerelative(feature)
result = require
relativewithoutcallback(feature)
Kernel.loaded(feature) if result
result
end

# bar/useit.rb
load 'foo/require
relative.rb'
require_relative "anything"

Produces

LoadError: cannot load such file -- foo/anything

But it should be (({bar/anything})).

Your suggestion would allow me to re-implement #relative_require. I know how to do that, I've done it before for Facets. But I've never felt it a very robust solution.
=end

#9 Updated by Boris Stitnicky over 1 year ago

trans (Thomas Sawyer) wrote:

You mean "cherrypicking like Facets" right? ;)

Sorry, I'm a noob, not knowing whom to credit :]

#10 Updated by Thomas Sawyer over 1 year ago

I just release a gem called backload that basically handles this. But in working through the problem set, it's become clear that there are some features of a good load callback that just can't be done in pure Ruby. I've already mentioned the #relative_require issue. Another issue, is that it would be nice to get the full path of the loaded file, e.g. as an additional option called :fullpath. There's no way to do that in pure Ruby b/c there is no way to lookup a load path (see issue #6376). Even if there were, it would be redundant b/c Ruby is just going to look it up again in the C code.

So putting it all together, it seems this really needs to be implemented in the C if it is to be done right. I do not know if that would mean it cannot be an optional standard library, or not.

I almost did not bother to release backload (and I still have half a mind to yank it) b/c it can't be as complete and solid an implementation as it really ought, but I suppose for some uses it would suffice.

http://rubyworks.github.com/backload

#11 Updated by Koichi Sasada about 1 year ago

  • Category set to core
  • Assignee set to Yukihiro Matsumoto

I'm sorry I don't catch up this discussion.
I assigned this ticket to matz, but I'm not sure matz is good person to handle it.
Please correct me.

Basically, I agree with this proposal.

Also available in: Atom PDF