abstractivator 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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