lab42_curry 0.1.1 → 0.2.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
  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