lab42_curry 0.1.1 → 0.2.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
  SHA256:
3
- metadata.gz: 364ab07ebc3ae04d88f52078f9685de77f1ac5f7acc22c0fdc14352c80e14c93
4
- data.tar.gz: fe68d0fa92be55bae32d4eee096ef0989a94381701e9a41a31b31c516977145b
3
+ metadata.gz: 81a83bce0a791069efb5bf01049fb137367565cc1289d2824a43aae3b4336a72
4
+ data.tar.gz: 66479962f8bd6be1d89c29794fa7d09d1a84971520b8cf277804f6c722d60f9d
5
5
  SHA512:
6
- metadata.gz: e98215ae8b0a6ea139afe52e4f98648e351f48c950f336231002b134837b15254687649357205c6942e9659a9c3966a0464b875cc6dce949b4410857fe7979ba
7
- data.tar.gz: 27948a35f86d517a2eb681a2d378b0d13fee608990d2f958eff1fc206551b3cfdc69d01ab09b4c2630eaf6761d9298a13bfd020eb703a1135b49be05d422c282
6
+ metadata.gz: aadb1fda7c8a06586ce81fbc43e6d3f21fa20ec7c56297265f6a8330cc088740f3c197f7eac25e76a5a7bfc94d8267b7bc31a267ceb1cdaf9657196ff12a6554
7
+ data.tar.gz: '08c4b31e4d94aa2cd8a943e52a753b330cfbfacc402ae5ba22cf5d1cb4caea222d4a11983e33f5da4a1f52898797a8d966d6cf3f50f15e3f167f11f2bdc96c89'
data/README.md CHANGED
@@ -54,8 +54,8 @@ Then we see that
54
54
 
55
55
  Given the total reorder form
56
56
  ```ruby
57
- let(:twohundred_three) { curry(:adder, runtime_arg(2), runtime_arg(0), 1) }
58
- # now first argument is c (index 2) and second a (index 0) and b = 1
57
+ let(:twohundred_three) { curry(:adder, runtime_arg(1), 1, runtime_arg(0)) }
58
+ # now first argument is c (index 1) and second a (index 0) and b = 1
59
59
  # Like Elixir's &adder(&2, 1, &1)
60
60
  ```
61
61
  Then we have
@@ -134,8 +134,106 @@ But we can create a more lenient curry with `curry!`
134
134
  .to eq([1, 2, 0, :blue])
135
135
  ```
136
136
 
137
+ ### Context Currying Blocks
137
138
 
139
+ Often times it is the block which might be the fixed point in a series of computations, for
140
+ that reason we will curry a block
138
141
 
142
+ Given a function that takes a block
143
+ ```ruby
144
+ let(:sub_with) { ->(a, b, &blk) { blk.(a - b) } }
145
+ let(:double_diff) { curry( sub_with ) { _1 * 2 } }
146
+ ```
147
+ Then it does just that
148
+ ```ruby
149
+ expect( double_diff.(22, 1) ).to eq(42)
150
+ ```
151
+ And of course we can also curry a positional argument
152
+ ```ruby
153
+ triple_dec = curry( sub_with, rt_arg, 1 ) { _1 * 3 }
154
+ expect( triple_dec.(15) ).to eq(42)
155
+ ```
156
+
157
+ #### Context Currying on Unbound Methods
158
+
159
+ Can be a very useful exercise, we will see that `curry` creates a curred function that will bind when called
160
+
161
+ Given the classical map example:
162
+ ```ruby
163
+ let(:incrementer) { curry(Enumerable.instance_method(:map)) { _1 + 1} }
164
+ ```
165
+
166
+ Then we can use it by binding it to an `Enumerable` object
167
+ ```ruby
168
+ expect( incrementer.([1, 2]) ).to eq([2, 3])
169
+ ```
170
+
171
+ But we must not provide a block **again**
172
+ ```ruby
173
+ expect{ incrementer.([]) {_1 + 2} }
174
+ .to raise_error(
175
+ Lab42::Curry::DuplicateBlock,
176
+ "block has already been curried")
177
+ ```
178
+
179
+ This again can be authorized by using the more lenient `curry!` version
180
+ And therefor
181
+ ```ruby
182
+ maybe_incrementer = curry!(Enumerable.instance_method(:map)) {_1 + 1}
183
+ expect( maybe_incrementer.([1, 2], &:itself) ).to eq([1, 2])
184
+ ```
185
+
186
+ But of course we can also bind to unbound methods w/o a block
187
+
188
+ Given
189
+ ```ruby
190
+ let(:length) { curry(String.instance_method(:size)) }
191
+ ```
192
+ Then we can call it in a functional way
193
+ ```ruby
194
+ expect( length.("") ).to be_zero
195
+ ```
196
+
197
+ ### Context Computed Arguments
198
+
199
+ Let us say we want to define a specialisation of a function
200
+ Given
201
+ ```ruby
202
+ def add a, b, c
203
+ a + b + c
204
+ end
205
+ let(:adddiff) { curry(:add, comp{ _2 - _3}) }
206
+ ```
207
+ Then the first parameter will be the diff of the second and third argument
208
+ ```ruby
209
+ expect( adddiff.(21, 1) ).to eq(42)
210
+ ```
211
+
212
+ When we specify a position for `comp` the computed argument gets this place
213
+ ```ruby
214
+ def args *a
215
+ a
216
+ end
217
+ let(:sum_middle) { curry(:args, comp(1){ _1 + _3 } ) }
218
+ ```
219
+ Then we get
220
+ ```ruby
221
+ expect( sum_middle.(1, 2) ).to eq([1, 3, 2])
222
+ ```
223
+
224
+ #### Context Computed Keywords
225
+
226
+ Given
227
+ ```ruby
228
+ def kwds *, **k
229
+ k
230
+ end
231
+ let(:sum) { curry(:kwds, a: comp{ _1 + _2}) }
232
+ ```
233
+ Then we can expect that sum corresponds
234
+ ```ruby
235
+ expect( sum.(1, 2) ).to eq({a: 3})
236
+ ```
139
237
  # LICENSE
140
238
 
141
239
  Copyright 2020,1 Robert Dober robert.dober@gmail.com
@@ -1,18 +1,16 @@
1
- require_relative "curry/data"
2
- require_relative "curry/runtime_arg"
3
1
  require_relative "curry/compiletime_args"
2
+ require_relative "curry/computed_arg"
3
+ require_relative "curry/currier"
4
+ require_relative "curry/runtime_arg"
4
5
 
5
6
  module Lab42
6
7
  module Curry
7
8
  def curry(method_or_name, *curry_time_args, **curry_time_kwds, &blk)
8
- curry_data = Data.new(self, method_or_name, *curry_time_args, **curry_time_kwds, &blk)
9
- curry_data.curried
9
+ Currier.new(method_or_name, curry_time_args, curry_time_kwds, context: self, &blk)
10
10
  end
11
11
 
12
12
  def curry!(method_or_name, *curry_time_args, **curry_time_kwds, &blk)
13
- curry_data = Data.new(self, method_or_name, *curry_time_args, **curry_time_kwds, &blk)
14
- curry_data.allow_kwd_override = true
15
- curry_data.curried
13
+ Currier.new(method_or_name, curry_time_args, curry_time_kwds, context: self, allow_override: true, &blk)
16
14
  end
17
15
 
18
16
  def compiletime_args(positions)
@@ -20,8 +18,13 @@ module Lab42
20
18
  end
21
19
  alias_method :ct_args, :compiletime_args
22
20
 
23
- def runtime_arg(*args)
24
- RuntimeArg.new(*args)
21
+ def compute_arg(position = nil, &blk)
22
+ ComputedArg.new(position, blk)
23
+ end
24
+ alias_method :comp, :compute_arg
25
+
26
+ def runtime_arg position=nil
27
+ RuntimeArg.new position
25
28
  end
26
29
  alias_method :rt_arg, :runtime_arg
27
30
  end
@@ -1,76 +1,76 @@
1
- require_relative 'runtime_arg'
1
+ require_relative 'arg_compiler/positionals'
2
+ require_relative 'arg_compiler/phase2'
2
3
  require_relative 'compiletime_args'
4
+ require_relative 'computed_arg'
5
+ require_relative 'runtime_arg'
6
+
3
7
  require_relative 'errors'
4
8
 
5
9
  module Lab42
6
10
  module Curry
7
11
  class ArgCompiler
8
12
 
9
- def compile_args rt_args
10
- @__compiled_args__ ||= _compile_args(rt_args)
11
- end
13
+ attr_reader :ct_blk
12
14
 
13
- private
14
- def initialize(ct_args)
15
- _init
16
- _determine_predefined_positions! ct_args
15
+ def allows_override?; @allow_override end
16
+
17
+ def compile(rt_args, rt_kwds, rt_blk)
18
+ Phase2.new(self, rt_args, rt_kwds, rt_blk).compile
17
19
  end
18
20
 
19
- def _compile_args rt_args
20
- @rt_args = rt_args
21
- @rt_arg_positions.each(&method(:_set_rt_arg))
22
- @rt_args.each(&method(:_set_final!))
23
- @final.map.sort_by(&:first).map(&:last)
21
+ #
22
+ # 0 based
23
+ #
24
+ def positionals
25
+ @__positionals__ ||= Positionals.new
24
26
  end
25
27
 
26
- def _determine_predefined_positions! ct_args
27
- ct_args.each(&method(:_set_ct_arg))
28
+ def final_kwds
29
+ @__final_kwds__ ||= {}
28
30
  end
29
31
 
30
- def _init
31
- @rt_arg_positions = []
32
- # invariant @pos is lowest free index in @final
33
- # update both together with _set_final!( value, idx = @pos )
34
- @pos = 0
35
- @final = {}
32
+ private
33
+ def initialize(ct_args, ct_kwds, allow_override:, &blk)
34
+ @allow_override = allow_override
35
+ @ct_args = ct_args
36
+ @ct_blk = blk
37
+ @ct_kwds = ct_kwds
38
+
39
+ _precompile!
36
40
  end
37
41
 
38
- def _set_ct_arg ct_arg
42
+ def _ct_arg ct_arg
39
43
  case ct_arg
40
44
  when RuntimeArg
41
- _set_target_position! ct_arg.position
45
+ positionals.set_runtime_arg ct_arg
42
46
  when CompiletimeArgs
43
47
  _set_positions! ct_arg
48
+ when ComputedArg
49
+ positionals.set_computation ct_arg
44
50
  else
45
51
  _set_final! ct_arg
46
52
  end
47
53
  end
48
54
 
49
- def _set_final! value, pos=nil
50
- pos ||= @pos
51
- raise DuplicatePositionSpecification, "There is already a curried value for #{pos}" if @final.has_key?(pos)
52
- @final[pos] = value
53
- while @final.has_key?(@pos)
54
- @pos += 1
55
- end
55
+ def _ct_kwd((key, val))
56
+ final_kwds[key] = val
56
57
  end
57
58
 
58
- def _set_position!((position, value))
59
- _set_final! value, position
59
+ def _precompile!
60
+ @ct_args.each(&method(:_ct_arg))
61
+ @ct_kwds.each(&method(:_ct_kwd))
60
62
  end
61
63
 
62
- def _set_positions! ct_arg
63
- ct_arg.each(&method(:_set_position!))
64
+ def _set_final! value, pos=nil
65
+ positionals.set_value! value, pos
64
66
  end
65
67
 
66
- def _set_rt_arg rt_arg_position
67
- raise MissingRuntimeArg, "for position #{rt_arg_position}" if @rt_args.empty?
68
- @final[rt_arg_position] = @rt_args.shift
68
+ def _set_position!((position, value))
69
+ positionals.set_value! value, position
69
70
  end
70
71
 
71
- def _set_target_position! position
72
- @rt_arg_positions << (position || @pos)
73
- _set_final! RuntimeArg, position
72
+ def _set_positions! ct_arg
73
+ ct_arg.each(&method(:_set_position!))
74
74
  end
75
75
 
76
76
  end
@@ -0,0 +1,75 @@
1
+ require_relative '../errors'
2
+ require_relative 'positionals'
3
+ module Lab42
4
+ module Curry
5
+ class ArgCompiler
6
+ class Phase2
7
+
8
+ attr_reader :compiler, :rt_args, :rt_blk, :rt_kwds
9
+
10
+ # TODO: Easy, peasy sequential refactor
11
+ def compile
12
+ if rt_blk && compiler.ct_blk && !compiler.allows_override?
13
+ raise Lab42::Curry::DuplicateBlock, "block has already been curried"
14
+ end
15
+ @blk = rt_blk || compiler.ct_blk
16
+ @kwds = compiler.final_kwds.merge(rt_kwds){ |k, old, new|
17
+ raise Lab42::Curry::DuplicateKeywordArgument,
18
+ "keyword argument :#{k} is already defined with value #{old.inspect} cannot override with #{new.inspect}" unless compiler.allows_override?
19
+ new
20
+ }
21
+ @args = compiler.positionals.export_args
22
+ @first_free = 0
23
+ rt_args.each_with_index do |rt_arg, idx|
24
+ computed = compiler.positionals.computed(idx)
25
+ if computed
26
+ end
27
+ translated = compiler.positionals.translation(idx)
28
+ if translated
29
+ @args[translated] = rt_arg
30
+ else
31
+ # @args[idx + some_dynamic_offset] = rt_arg ???
32
+ _occupy_first_free rt_arg
33
+ end
34
+ end
35
+ compiler.positionals.computations do |idx, computed_arg|
36
+ @args[idx] = _compute(computed_arg)
37
+ end
38
+ @kwds.each do |kwd, val|
39
+ case val
40
+ when ComputedArg
41
+ @kwds[kwd] = _compute(val)
42
+ end
43
+ end
44
+ # TODO: Raise ArgumentError if holes are left in @args, but for that reason we must make compiler.final_args compact
45
+ # and indicate wholes with RuntimeArg, not sure this is done same for holes in @kwds
46
+ [@args, @kwds, @blk]
47
+ end
48
+
49
+ private
50
+
51
+ def initialize(compiler, rt_args, rt_kwds, rt_blk)
52
+ @compiler = compiler
53
+ @rt_args = rt_args
54
+ @rt_blk = rt_blk
55
+ @rt_kwds = rt_kwds
56
+ end
57
+
58
+ def _compute computed_arg
59
+ computed_arg.(*@args, **@kwds)
60
+ end
61
+
62
+ def _occupy_first_free with_value
63
+ return @args << with_value unless @first_free
64
+ @first_free =
65
+ (@first_free..@args.size).find {|idx| @args[idx] == RuntimeArg}
66
+ if @first_free
67
+ @args[@first_free] = with_value
68
+ else
69
+ @args << with_value
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,96 @@
1
+ require_relative '../errors'
2
+ require_relative '../runtime_arg'
3
+ # Represents the positionitional arguments as
4
+ # a hash idx -> value with operations to
5
+ # fill the "holes" with args provisioned
6
+ # at runtime.
7
+ # The filling of the holes is done at runtime
8
+ # by translating runtime argument indices with
9
+ # the @translations hash.
10
+ module Lab42
11
+ module Curry
12
+ class ArgCompiler
13
+ class Positionals
14
+
15
+ attr_reader :ct_pos, :rt_pos
16
+
17
+ def computations
18
+ @computations.each { |idx, comp| yield idx, comp }
19
+ end
20
+
21
+ def computed idx
22
+ @computations[idx]
23
+ end
24
+
25
+ def export_args
26
+ (0...(@args.keys.max&.succ||0))
27
+ .inject [] do |result, idx|
28
+ if @args.has_key?(idx)
29
+ result << @args[idx]
30
+ else
31
+ result << RuntimeArg
32
+ end
33
+ result
34
+ end
35
+ end
36
+
37
+ def set_computation comp_arg
38
+ @args[comp_arg.position || ct_pos] = comp_arg.class
39
+ @computations[comp_arg.position || rt_pos] = comp_arg
40
+ end
41
+
42
+ # An rt_arg placeholder
43
+ def set_runtime_arg rt_arg
44
+ @args[ct_pos] = rt_arg.class
45
+ @translations[rt_arg.position || rt_pos] = ct_pos
46
+ _update_positions
47
+ end
48
+
49
+ # A curried value or computation, occurs from `ct_args` or literal values in which case `ct_pos` is `nil`
50
+ def set_value! value, ct_pos=nil
51
+ ct_pos ||= @ct_pos
52
+ raise Lab42::Curry::DuplicatePositionSpecification,
53
+ "There is already a curried value #{@args[ct_pos].inspect} at index #{ct_pos}" if _occupied?(ct_pos)
54
+ @args[ct_pos] = value
55
+
56
+ (@ct_pos..ct_pos.pred).each(&method(:_occupy))
57
+ # TODO: Need to check if we have to fill the holes with RuntimeArg
58
+ # and allow @ct_pos to point to present RuntimeArg values?
59
+
60
+ _update_positions
61
+ end
62
+
63
+ def translation idx
64
+ @translations[idx]
65
+ end
66
+
67
+ private
68
+ def initialize
69
+ @args = {}
70
+ @computations = {}
71
+ @ct_pos = 0
72
+ @rt_pos = 0
73
+ @translations = {}
74
+ end
75
+
76
+ def _occupied? key
77
+ @args.has_key?(key) && @args[key] != RuntimeArg
78
+ end
79
+
80
+ def _occupy idx
81
+ @args[idx] = RuntimeArg unless @args.has_key?(idx)
82
+ end
83
+
84
+ def _update_positions
85
+ while @args.has_key?(@ct_pos)
86
+ @ct_pos += 1
87
+ end
88
+ while @translations.has_key?(@rt_pos)
89
+ @rt_pos += 1
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+
@@ -0,0 +1,18 @@
1
+ module Lab42
2
+ module Curry
3
+ class ComputedArg
4
+
5
+ attr_reader :position
6
+
7
+ def call(*args, **kwds)
8
+ @blk.call(*args, **kwds)
9
+ end
10
+
11
+ private
12
+ def initialize(position, blk)
13
+ @blk = blk
14
+ @position = position
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,61 @@
1
+ require_relative 'arg_compiler'
2
+
3
+ module Lab42
4
+ module Curry
5
+ class Currier
6
+ attr_reader :arg_compiler, :context, :ct_args, :ct_blk, :ct_kwds, :mthd
7
+
8
+ def call(*args, **kwds, &blk)
9
+ case mthd
10
+ when UnboundMethod
11
+ _bind_and_call(args, kwds, blk)
12
+ else
13
+ _just_call(args, kwds, blk)
14
+ end
15
+ end
16
+
17
+ def _bind_and_call(args, kwds, blk)
18
+ receiver = args.shift
19
+ mthd = @mthd.bind(receiver)
20
+ rt_args, rt_kwds, rt_blk = arg_compiler.compile(args, kwds, blk)
21
+ if rt_blk
22
+ mthd.(*rt_args, **rt_kwds, &rt_blk)
23
+ else
24
+ mthd.(*rt_args, **rt_kwds)
25
+ end
26
+ end
27
+
28
+ def _just_call(args, kwds, blk)
29
+ rt_args, rt_kwds, rt_blk = arg_compiler.compile(args, kwds, blk)
30
+ if rt_blk
31
+ mthd.(*rt_args, **rt_kwds, &rt_blk)
32
+ else
33
+ mthd.(*rt_args, **rt_kwds)
34
+ end
35
+ end
36
+
37
+ private
38
+ def initialize(method_or_name, ct_args, ct_kwds, context:, allow_override: false, &ct_blk)
39
+ @allow_override = allow_override
40
+ @context = context
41
+ @ct_args = ct_args
42
+ @ct_blk = ct_blk
43
+ @ct_kwds = ct_kwds
44
+
45
+ @arg_compiler = ArgCompiler.new(ct_args, ct_kwds, allow_override: allow_override, &ct_blk)
46
+
47
+ _init_mthd method_or_name
48
+ end
49
+
50
+ def _init_mthd method_or_name
51
+ @mthd =
52
+ case method_or_name
53
+ when Symbol
54
+ context.method(method_or_name)
55
+ else
56
+ method_or_name
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,7 +1,8 @@
1
1
  module Lab42
2
2
  module Curry
3
- DuplicatePositionSpecification = Class.new RuntimeError
3
+ DuplicateBlock = Class.new RuntimeError
4
4
  DuplicateKeywordArgument = Class.new RuntimeError
5
+ DuplicatePositionSpecification = Class.new RuntimeError
5
6
  MissingRuntimeArg = Class.new RuntimeError
6
7
  end
7
8
  end
@@ -1,5 +1,5 @@
1
1
  module Lab42
2
2
  module Curry
3
- VERSION = "0.1.1"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lab42_curry
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Dober
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-27 00:00:00.000000000 Z
11
+ date: 2021-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry
@@ -63,8 +63,11 @@ files:
63
63
  - README.md
64
64
  - lib/lab42/curry.rb
65
65
  - lib/lab42/curry/arg_compiler.rb
66
+ - lib/lab42/curry/arg_compiler/phase2.rb
67
+ - lib/lab42/curry/arg_compiler/positionals.rb
66
68
  - lib/lab42/curry/compiletime_args.rb
67
- - lib/lab42/curry/data.rb
69
+ - lib/lab42/curry/computed_arg.rb
70
+ - lib/lab42/curry/currier.rb
68
71
  - lib/lab42/curry/errors.rb
69
72
  - lib/lab42/curry/runtime_arg.rb
70
73
  - lib/lab42/curry/version.rb
@@ -1,44 +0,0 @@
1
- require_relative 'arg_compiler'
2
-
3
- module Lab42
4
- module Curry
5
- class Data
6
- attr_accessor :allow_kwd_override
7
- attr_reader :context, :ct_args, :ct_kwds, :curried
8
-
9
- private
10
- def initialize(ctxt, method_or_name, *curry_time_args, **curry_time_kwds, &blk)
11
- @allow_kwd_override
12
- @context = ctxt
13
- @ct_args = curry_time_args
14
- @ct_kwds = curry_time_kwds
15
-
16
- @mthd =
17
- case method_or_name
18
- when Symbol
19
- context.method(method_or_name)
20
- else
21
- method_or_name
22
- end
23
-
24
- @arg_compiler = ArgCompiler.new(ct_args)
25
- _curry(&blk)
26
- end
27
-
28
- def _curry(&blk)
29
- @curried = ->(*rt_args, **rt_kwds) do
30
- @mthd.(
31
- *@arg_compiler.compile_args(rt_args),
32
- **@ct_kwds.merge(rt_kwds) { |kwd, old_val, new_val|
33
- if allow_kwd_override
34
- new_val
35
- else
36
- raise DuplicateKeywordArgument, "keyword argument #{kwd.inspect} is already defined with value #{old_val.inspect} cannot override with #{new_val.inspect}"
37
- end
38
- }
39
- )
40
- end
41
- end
42
- end
43
- end
44
- end