abstractivator 0.13.0 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 45c8fba3280c1d29b04f039b41ad5f9f1fb0d9ce
4
- data.tar.gz: 466908745cf321c32d574ab98df30f0329656fba
3
+ metadata.gz: aa5f0441439d94fb15db90acbcc41c99f378d5fd
4
+ data.tar.gz: 66885acd1df0f4322a21dcbedf41da2028416604
5
5
  SHA512:
6
- metadata.gz: 8ca79e31fdece1d82be3be63147f30ec618ce81b3290d5418778ff5620604e9dbaab86dbe4f6c8181bfbbfb23792cff7def8fe2b73263fdcb2c28dc314b9093b
7
- data.tar.gz: f9a2a806b5a48721bd2969cb9e9f38826b33d0db14794ed4072f05493333414d6b57ba4c81ad2d2fd8eeb6f318950cd45aec3d229e4c72b4e551c5f706bf1313
6
+ metadata.gz: db0062035f743c6a51c716ff3b52db47ed96f16b96333d858f0e698d1d864153cabcff4f5bad10ab4f6ad75101ef0742a4199e57f2cfef37328e116b16596d8f
7
+ data.tar.gz: e120d0ba2180cb4dda23e6d9a9a57043b277a2cab2fd8c720a7c96b6c8e73f86ac1e4ec834b2f4db5477339c5309ff479d36c2599f14e50dfce939a5d3daf5d7
@@ -9,8 +9,8 @@ module Enumerable
9
9
  # returns an array of 2-element arrays, each of which is a left/right pair.
10
10
  def self.outer_join(left, right, get_left_key, get_right_key, left_default, right_default)
11
11
 
12
- ls = left.hash_map(get_left_key)
13
- rs = right.hash_map(get_right_key)
12
+ ls = left.inject({}) { |h, x| h.store(get_left_key.call(x), x); h } # Avoid #hash_map and Array#to_h
13
+ rs = right.inject({}) { |h, x| h.store(get_right_key.call(x), x); h } # for better performance
14
14
 
15
15
  raise 'duplicate left keys' if ls.size < left.size
16
16
  raise 'duplicate right keys' if rs.size < right.size
@@ -68,29 +68,56 @@ class Proc
68
68
  end
69
69
  end
70
70
 
71
- # tries to coerce x into a procedure, then calls it with
72
- # the given argument list.
71
+ LooseCallInfo = Struct.new(:params, :accepts_arg_splat, :total_arity, :req_arity,
72
+ :requires_kw_customization, :all_key_names, :kw_padding)
73
+
74
+ # Tries to coerce x into a procedure, then calls it with the given argument list.
73
75
  # If x cannot be coerced into a procedure, returns x.
76
+ # This method is optimized for use cases typically found in tight loops,
77
+ # namely where x is either a symbol or a keyword-less fixed-arity proc.
78
+ # It attempts to minimize the number of intermediate arrays created for these cases
79
+ # (as would be produced by calls to #map, #select, #take, #pad_right, etc.)
80
+ # CPU overhead created by loose_call is bad, but unexpected memory consumption would
81
+ # be worse, considering Proc#call has zero memory footprint.
82
+ # These optimizations produce a ~5x speedup, which is still 2-4x slower than
83
+ # regular Proc#call.
74
84
  def self.loose_call(x, args, kws={}, &block)
85
+ return x.to_proc.call(*args) if x.is_a?(Symbol) # optimization for a typical use case
75
86
  x = x.to_proc if x.respond_to?(:to_proc)
76
- x.callable? or return x
77
- arg_types = x.parameters.map(&:first)
87
+ return x unless x.callable?
88
+
89
+ # cache proc info for performance
90
+ info = x.instance_variable_get(:@loose_call_info)
91
+ unless info
92
+ params = x.parameters
93
+ info = LooseCallInfo.new
94
+ info.params = params
95
+ info.req_arity = params.count { |p| p.first == :req }
96
+ info.total_arity = info.req_arity + params.count { |p| p.first == :opt }
97
+ info.accepts_arg_splat = params.any? { |p| p.first == :rest }
98
+ accepts_kw_splat = params.any? { |p| p.first == :keyrest }
99
+ has_kw_args = params.any? { |(type, name)| (type == :key || type == :keyreq) && !name.nil? }
100
+ info.requires_kw_customization = has_kw_args && !accepts_kw_splat
101
+ if info.requires_kw_customization
102
+ opt_key_names = info.params.select { |(type, name)| type == :key && !name.nil? }.map(&:value)
103
+ req_key_names = info.params.select { |(type, name)| type == :keyreq && !name.nil? }.map(&:value)
104
+ info.all_key_names = opt_key_names + req_key_names
105
+ info.kw_padding = req_key_names.hash_map { nil }
106
+ end
107
+ x.instance_variable_set(:@loose_call_info, info)
108
+ end
109
+
78
110
  # customize args
79
- req_arity = arg_types.select{|x| x == :req}.size
80
- total_arity = req_arity + arg_types.select{|x| x == :opt}.size
81
- accepts_arg_splat = arg_types.include?(:rest)
82
- unless accepts_arg_splat
83
- args = args.take(total_arity).pad_right(req_arity)
111
+ unless info.accepts_arg_splat
112
+ args = args.take(info.total_arity) if args.size > info.total_arity
113
+ args = args.pad_right(info.req_arity) if args.size < info.req_arity
84
114
  end
115
+
85
116
  # customize keywords
86
- accepts_kw_splat = arg_types.include?(:keyrest)
87
- unless accepts_kw_splat
88
- opt_key_names = x.parameters.select{|(type, name)| type == :key && !name.nil?}.map(&:value)
89
- req_key_names = x.parameters.select{|(type, name)| type == :keyreq && !name.nil?}.map(&:value)
90
- all_key_names = opt_key_names + req_key_names
91
- padding = req_key_names.hash_map{nil}
92
- kws = padding.merge(kws.select{|k| all_key_names.include?(k)})
117
+ if info.requires_kw_customization
118
+ kws = info.kw_padding.merge(kws.select { |k| info.all_key_names.include?(k) })
93
119
  end
120
+
94
121
  if kws.any?
95
122
  x.call(*args, **kws, &block)
96
123
  else
@@ -1,3 +1,3 @@
1
1
  module Abstractivator
2
- VERSION = '0.13.0'
2
+ VERSION = '0.14.0'
3
3
  end
@@ -62,7 +62,6 @@ context 'in the world of functional programming' do
62
62
  expect(events).to eql [0, 1, 2, 3] # lambdas are strict in arity,
63
63
  # so if their bodies executed, then we know
64
64
  # they were called with the correct number of arguments
65
-
66
65
  end
67
66
  it 'pads with nils' do
68
67
  verify_called_with ->(a, b) {[a, b]},
@@ -120,11 +119,18 @@ context 'in the world of functional programming' do
120
119
  end
121
120
 
122
121
  def verify_called_with(p, input_args, output)
123
- expect(do_call(p, input_args, {})).to eql output
122
+ 2.times do
123
+ # Run twice to ensure the LooseCallInfo caching works.
124
+ # This is not paranoia; there actually was a bug thanks
125
+ # to ruby's sloppy scope.
126
+ expect(do_call(p, input_args, {})).to eql output
127
+ end
124
128
  end
125
129
 
126
130
  def verify_called_with_keywords(p, input_kws, output)
127
- expect(do_call(p, [], input_kws)).to eql output
131
+ 2.times do
132
+ expect(do_call(p, [], input_kws)).to eql output
133
+ end
128
134
  end
129
135
  end
130
136
 
@@ -138,6 +144,33 @@ context 'in the world of functional programming' do
138
144
  expect(Proc.loose_call(:to_s, ['5'])).to eql '5'
139
145
  end
140
146
 
147
+ # # Uncomment this to test performance. It runs several typical
148
+ # # scenarios through both Proc#call and Proc#loose_call to
149
+ # # evaluate relative performance.
150
+ # # As of this commit, loose_call was 2-4x slower.
151
+ # it 'has reasonable performance' do
152
+ # require 'benchmark'
153
+ # Benchmark.bm do |bm|
154
+ # bench = proc do |label, proc_ish, args, kws|
155
+ # bm.report("call #{label}") do
156
+ # proc_ish = proc_ish.to_proc if proc_ish.is_a?(Symbol)
157
+ # if kws.none?
158
+ # 1_000_000.times { proc_ish.call(*args) }
159
+ # else
160
+ # 1_000_000.times { proc_ish.call(*args, **kws) }
161
+ # end
162
+ # end
163
+ # bm.report("loose_call #{label}") do
164
+ # 1_000_000.times { Proc.loose_call(proc_ish, args, kws) }
165
+ # end
166
+ # end
167
+ # bench.('symbol ', :to_s, [42], {})
168
+ # bench.('proc ', proc { |x| x.to_s }, [42], {})
169
+ # bench.('lambda ', lambda { |x| x.to_s }, [42], {})
170
+ # bench.('keyword', proc { |a:| a.to_s }, [], {a: 42})
171
+ # end
172
+ # end
173
+
141
174
  def do_call(p, args, kws={}, &block)
142
175
  Proc.loose_call(p, args, kws, &block)
143
176
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: abstractivator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Winton
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-02-16 00:00:00.000000000 Z
11
+ date: 2017-04-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler