lib/abstractivator/proc_ext.rb in abstractivator-0.13.0 vs lib/abstractivator/proc_ext.rb in abstractivator-0.14.0

- old
+ new

@@ -66,32 +66,59 @@ else call(*args, &block) end end - # tries to coerce x into a procedure, then calls it with - # the given argument list. + LooseCallInfo = Struct.new(:params, :accepts_arg_splat, :total_arity, :req_arity, + :requires_kw_customization, :all_key_names, :kw_padding) + + # Tries to coerce x into a procedure, then calls it with the given argument list. # If x cannot be coerced into a procedure, returns x. + # This method is optimized for use cases typically found in tight loops, + # namely where x is either a symbol or a keyword-less fixed-arity proc. + # It attempts to minimize the number of intermediate arrays created for these cases + # (as would be produced by calls to #map, #select, #take, #pad_right, etc.) + # CPU overhead created by loose_call is bad, but unexpected memory consumption would + # be worse, considering Proc#call has zero memory footprint. + # These optimizations produce a ~5x speedup, which is still 2-4x slower than + # regular Proc#call. def self.loose_call(x, args, kws={}, &block) + return x.to_proc.call(*args) if x.is_a?(Symbol) # optimization for a typical use case x = x.to_proc if x.respond_to?(:to_proc) - x.callable? or return x - arg_types = x.parameters.map(&:first) + return x unless x.callable? + + # cache proc info for performance + info = x.instance_variable_get(:@loose_call_info) + unless info + params = x.parameters + info = LooseCallInfo.new + info.params = params + info.req_arity = params.count { |p| p.first == :req } + info.total_arity = info.req_arity + params.count { |p| p.first == :opt } + info.accepts_arg_splat = params.any? { |p| p.first == :rest } + accepts_kw_splat = params.any? { |p| p.first == :keyrest } + has_kw_args = params.any? { |(type, name)| (type == :key || type == :keyreq) && !name.nil? } + info.requires_kw_customization = has_kw_args && !accepts_kw_splat + if info.requires_kw_customization + opt_key_names = info.params.select { |(type, name)| type == :key && !name.nil? }.map(&:value) + req_key_names = info.params.select { |(type, name)| type == :keyreq && !name.nil? }.map(&:value) + info.all_key_names = opt_key_names + req_key_names + info.kw_padding = req_key_names.hash_map { nil } + end + x.instance_variable_set(:@loose_call_info, info) + end + # customize args - req_arity = arg_types.select{|x| x == :req}.size - total_arity = req_arity + arg_types.select{|x| x == :opt}.size - accepts_arg_splat = arg_types.include?(:rest) - unless accepts_arg_splat - args = args.take(total_arity).pad_right(req_arity) + unless info.accepts_arg_splat + args = args.take(info.total_arity) if args.size > info.total_arity + args = args.pad_right(info.req_arity) if args.size < info.req_arity end + # customize keywords - accepts_kw_splat = arg_types.include?(:keyrest) - unless accepts_kw_splat - opt_key_names = x.parameters.select{|(type, name)| type == :key && !name.nil?}.map(&:value) - req_key_names = x.parameters.select{|(type, name)| type == :keyreq && !name.nil?}.map(&:value) - all_key_names = opt_key_names + req_key_names - padding = req_key_names.hash_map{nil} - kws = padding.merge(kws.select{|k| all_key_names.include?(k)}) + if info.requires_kw_customization + kws = info.kw_padding.merge(kws.select { |k| info.all_key_names.include?(k) }) end + if kws.any? x.call(*args, **kws, &block) else x.call(*args, &block) end