mug 2.0.0 → 2.1.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: ae2889ece035a30a218babac3772fe51854ade8ef1164885502bd5d3ff6bb39b
4
- data.tar.gz: deb9c3a27eb1cf6684b56fe6dfc1fbb9318a33ba432ea5c5cd714917a76bd563
3
+ metadata.gz: fb09c157ead059eeb09c4b9f0686ec84636d08cbb46755e838e561a21645aa1e
4
+ data.tar.gz: 06c8ee0f76b4763199876bdd76bf6a28ed987e588248c8683108c53409943e77
5
5
  SHA512:
6
- metadata.gz: 7734746e0817ef7ceb90a1a7742486993c2f6956d0f76ed3968e4edb6c8e7a181ffb148cb7de5e94e3cafe817632626e5a9a706ca889aee6796e9cf1a1f43811
7
- data.tar.gz: 7a1ce3c096ddf71af5c87eade7f4b727fd32af167ba83a8e42b98fa120609e4414e38341d981274fc8d383a3a43da65c8a1d7bf3f3bfabd1d42a3f0cabd34ca2
6
+ metadata.gz: 4a3e1a610087afa31dca23c0b4f72f3fe4959fd13f45e39c17ea86f058293926a97da12beceea748601a615bb1f10d5bcb0f6f6b3dc7b1c00a2b50702cc5a2b2
7
+ data.tar.gz: 169f4dba3371163df8f793ae66c0182fd29de7bff65546324e121466837d28d5d718277240eb859b54aefecc7063ff5c349009b85ad8f721028a5faad2b7dd5d
data/lib/mug/apply.rb CHANGED
@@ -1,14 +1,37 @@
1
-
2
1
  class Proc
3
- #
4
- # Curries this Proc and partially applies parameters.
5
- # If a sufficient number of arguments are supplied, it passes the
6
- # supplied arguments to the original proc and returns the result.
7
- # Otherwise, returns another curried proc that takes the rest of
8
- # arguments.
9
- #
10
- def apply(*args)
11
- curry.call(*args)
2
+ # TruffleRuby's Proc#curry does not perform partial application for
3
+ # non-lambda procs; it invokes the proc immediately regardless of the
4
+ # number of arguments supplied.
5
+ if RUBY_ENGINE == 'truffleruby'
6
+ #
7
+ # Curries this Proc and partially applies parameters.
8
+ # If a sufficient number of arguments are supplied, it passes the
9
+ # supplied arguments to the original proc and returns the result.
10
+ # Otherwise, returns another curried proc that takes the rest of
11
+ # arguments.
12
+ #
13
+ def apply(*args)
14
+ n = arity < 0 ? -arity - 1 : arity
15
+ if lambda?
16
+ curry(n).call(*args)
17
+ elsif args.length >= n
18
+ call(*args)
19
+ else
20
+ proc {|*more| call(*args, *more) }
21
+ end
22
+ end
23
+ else
24
+ #
25
+ # Curries this Proc and partially applies parameters.
26
+ # If a sufficient number of arguments are supplied, it passes the
27
+ # supplied arguments to the original proc and returns the result.
28
+ # Otherwise, returns another curried proc that takes the rest of
29
+ # arguments.
30
+ #
31
+ def apply(*args)
32
+ n = arity < 0 ? -arity - 1 : arity
33
+ curry(n).call(*args)
34
+ end
12
35
  end
13
36
  end
14
37
 
@@ -21,7 +44,8 @@ class Method
21
44
  # arguments.
22
45
  #
23
46
  def apply(*args)
24
- curry.call(*args)
47
+ n = arity < 0 ? -arity - 1 : arity
48
+ curry(n).call(*args)
25
49
  end
26
50
  end
27
51
 
@@ -0,0 +1,191 @@
1
+
2
+ class Proc
3
+
4
+ #
5
+ # Composes a sequence of functions.
6
+ #
7
+ # A function is anything that responds to #to_proc, so
8
+ # symbols are allowed.
9
+ #
10
+ # This proc is prepended at the start of the composition.
11
+ #
12
+ def compose *funcs
13
+ return self if funcs.empty?
14
+ self >> funcs.map(&:to_proc).reduce(:>>)
15
+ end
16
+
17
+ #
18
+ # Composes a sequence of functions.
19
+ #
20
+ # A function is anything that responds to #to_proc, so
21
+ # symbols are allowed.
22
+ #
23
+ # This proc is appended at the end of the composition.
24
+ #
25
+ def precompose *funcs
26
+ return self if funcs.empty?
27
+ self << funcs.map(&:to_proc).reduce(:>>)
28
+ end
29
+
30
+ #
31
+ # Applies this function to each element of +args+ in order.
32
+ #
33
+ # `proc.mapply(*args)` is equivalent to `args.map(&proc)`
34
+ #
35
+ def mapply *args
36
+ args.map {|*a| self.call(*a) }
37
+ end
38
+
39
+ #
40
+ # Generates a function that memoizes this one. For a given
41
+ # set of parameters, this proc is only invoked once; the
42
+ # result is remembered for subsequent invocations.
43
+ #
44
+ def memoize
45
+ cache = {}
46
+ lambda do |*args|
47
+ cache.fetch(args) {|_| cache[args] = self.call(*args) }
48
+ end
49
+ end
50
+ alias memoise memoize
51
+
52
+ #
53
+ # Generates a function that reorders its arguments according
54
+ # to +indices+ and calls this function on the resulting
55
+ # list.
56
+ #
57
+ # The +arity+ parameter controls how mismatches in length
58
+ # between the arguments and indices are handled:
59
+ # :min - cap at the minimum of args and indices (default)
60
+ # :max - use the maximum; nil-fill if args are short,
61
+ # pass-through if args are long
62
+ # :indices - always use indices.size; nil for out-of-bounds
63
+ # :arguments - always use args.size; excess positions pass
64
+ # through at their original index
65
+ #
66
+ def trans *indices, arity: :min
67
+ case arity
68
+ when :min
69
+ lambda do |*a|
70
+ n = [a.size, indices.size].min
71
+ list = (0...n).map {|i| a[indices[i]] }
72
+ self.call(*list)
73
+ end
74
+ when :indices
75
+ lambda do |*a|
76
+ list = (0...indices.size).map {|i| a[indices[i]] }
77
+ self.call(*list)
78
+ end
79
+ when :arguments
80
+ lambda do |*a|
81
+ list = (0...a.size).map do |i|
82
+ i < indices.size ? a[indices[i]] : a[i]
83
+ end
84
+ self.call(*list)
85
+ end
86
+ when :max
87
+ lambda do |*a|
88
+ n = [a.size, indices.size].max
89
+ list = (0...n).map do |i|
90
+ i < indices.size ? a[indices[i]] : a[i]
91
+ end
92
+ self.call(*list)
93
+ end
94
+ else
95
+ raise ArgumentError, "unknown arity mode: #{arity.inspect}"
96
+ end
97
+ end
98
+
99
+ #
100
+ # Generates a function that maps its arguments to each of
101
+ # +funcs+ in order.
102
+ #
103
+ def zipmap *funcs
104
+ lambda do |*args|
105
+ n = [args.size, funcs.size].min
106
+ mapped = (0...n).map do |i|
107
+ func = funcs[i]
108
+ arg = args[i]
109
+
110
+ if func.nil?
111
+ arg
112
+ elsif func.respond_to? :call
113
+ func.call arg
114
+ elsif func.is_a? Symbol
115
+ arg.__send__ func
116
+ else
117
+ raise TypeError, "expected callable, Symbol, or nil; got #{func.class}"
118
+ end
119
+ end
120
+ self.call(*mapped)
121
+ end
122
+ end
123
+
124
+ class << self
125
+
126
+ #
127
+ # Generates a function that maps its arguments to the
128
+ # given +funcs+ list in order.
129
+ #
130
+ def juxt *funcs
131
+ lambda do |*args|
132
+ funcs.map {|f| f.to_proc.call(*args) }
133
+ end
134
+ end
135
+
136
+ #
137
+ # Generates an identity function that always returns its argument exactly.
138
+ #
139
+ def identity
140
+ lambda {|x| x }
141
+ end
142
+
143
+ #
144
+ # Generates a constant function that always returns +c+.
145
+ #
146
+ # Note that it always returns the same object, so mutations will stick
147
+ # from invocation to invocation.
148
+ #
149
+ def const c
150
+ lambda {|*| c }
151
+ end
152
+
153
+ end
154
+ end
155
+
156
+ class Enumerator
157
+ class << self
158
+
159
+ #
160
+ # Creates an Enumerator that can unfold a sequence from a given seed.
161
+ #
162
+ def unfold(seed, &blk)
163
+ raise ArgumentError, 'no block given' unless block_given?
164
+ Enumerator.new do |y|
165
+ loop do
166
+ result = blk.call(seed)
167
+ break if result.nil?
168
+ value, seed = result
169
+ y << value
170
+ end
171
+ end
172
+ end
173
+
174
+ end
175
+ end
176
+
177
+ =begin
178
+ Copyright (c) 2014-2026, Matthew Kerwin <matthew@kerwin.net.au>
179
+
180
+ Permission to use, copy, modify, and/or distribute this software for any
181
+ purpose with or without fee is hereby granted, provided that the above
182
+ copyright notice and this permission notice appear in all copies.
183
+
184
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
185
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
186
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
187
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
188
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
189
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
190
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
191
+ =end
data/lib/mug.rb CHANGED
@@ -17,6 +17,7 @@ require_relative 'mug/enumerable/chain'
17
17
  require_relative 'mug/enumerable/counts'
18
18
  require_relative 'mug/enumerable/hash-like'
19
19
  require_relative 'mug/fragile-method-chain'
20
+ require_relative 'mug/functional'
20
21
  require_relative 'mug/hash/fetch-assign'
21
22
  require_relative 'mug/hash/map'
22
23
  require_relative 'mug/hash/merge'
@@ -15,7 +15,8 @@ class Test_enumerable_chain < Test::Unit::TestCase
15
15
  ]
16
16
 
17
17
  enum = a.chain(b, c)
18
- assert_kind_of( Enumerator, enum )
18
+ assert( enum.kind_of?(Enumerator) || enum.kind_of?(Enumerator::Chain),
19
+ "expected Enumerator or Enumerator::Chain, got #{enum.class}" )
19
20
 
20
21
  result = []
21
22
  enum.each do |*x|
@@ -0,0 +1,242 @@
1
+ require 'test/unit'
2
+ $VERBOSE = true
3
+
4
+ require_relative '../lib/mug/functional'
5
+
6
+ class Test_functional_compose < Test::Unit::TestCase
7
+ def test_compose__readme_example
8
+ prc = lambda {|x| x.inspect }
9
+ prc2 = prc.compose(:to_s, :length)
10
+ assert_equal( 3, prc2[123] )
11
+ assert_equal( 5, prc2['abc'] )
12
+ end
13
+ def test_compose__empty
14
+ prc = lambda {|x| x }
15
+ assert_same( prc, prc.compose )
16
+ end
17
+ def test_compose__single
18
+ prc = lambda {|x| x + 1 }
19
+ prc2 = prc.compose(:abs)
20
+ assert_equal( 4, prc2[-5] )
21
+ end
22
+ end
23
+
24
+ class Test_functional_precompose < Test::Unit::TestCase
25
+ def test_precompose__readme_example
26
+ prc = lambda {|x| x.inspect }
27
+ prc2 = prc.precompose(:to_s, :length)
28
+ assert_equal( '3', prc2[123] )
29
+ assert_equal( '3', prc2['abc'] )
30
+ end
31
+ def test_precompose__empty
32
+ prc = lambda {|x| x }
33
+ assert_same( prc, prc.precompose )
34
+ end
35
+ def test_precompose__single
36
+ prc = lambda {|x| x * 2 }
37
+ prc2 = prc.precompose(:to_i)
38
+ assert_equal( 246, prc2['123'] )
39
+ end
40
+ end
41
+
42
+ class Test_functional_mapply < Test::Unit::TestCase
43
+ def test_mapply__basic
44
+ prc = lambda {|x| x.to_s }
45
+ assert_equal( ['1', '2', '3'], prc.mapply(1, 2, 3) )
46
+ end
47
+ def test_mapply__equivalent_to_map
48
+ prc = lambda {|x| x * 2 }
49
+ args = [1, 2, 3, 4]
50
+ assert_equal( args.map(&prc), prc.mapply(*args) )
51
+ end
52
+ def test_mapply__empty
53
+ prc = lambda {|x| x }
54
+ assert_equal( [], prc.mapply )
55
+ end
56
+ end
57
+
58
+ class Test_functional_memoize < Test::Unit::TestCase
59
+ def test_memoize__caches_result
60
+ call_count = 0
61
+ prc = lambda do |x|
62
+ call_count += 1
63
+ x * 2
64
+ end
65
+ memo = prc.memoize
66
+ assert_equal( 4, memo.call(2) )
67
+ assert_equal( 1, call_count )
68
+ assert_equal( 4, memo.call(2) )
69
+ assert_equal( 1, call_count )
70
+ end
71
+ def test_memoize__different_args
72
+ call_count = 0
73
+ prc = lambda do |x|
74
+ call_count += 1
75
+ x * 2
76
+ end
77
+ memo = prc.memoize
78
+ assert_equal( 4, memo.call(2) )
79
+ assert_equal( 6, memo.call(3) )
80
+ assert_equal( 2, call_count )
81
+ end
82
+ def test_memoise__alias
83
+ prc = lambda {|x| x }
84
+ assert_equal( prc.method(:memoize), prc.method(:memoise) )
85
+ end
86
+ end
87
+
88
+ class Test_functional_trans < Test::Unit::TestCase
89
+ def test_trans__reorder
90
+ prc = lambda {|a, b, c| [a, b, c] }
91
+ prc2 = prc.trans(2, 0, 1)
92
+ assert_equal( [:c, :a, :b], prc2.call(:a, :b, :c) )
93
+ end
94
+ def test_trans__duplicate_index
95
+ prc = lambda {|a, b| [a, b] }
96
+ prc2 = prc.trans(0, 0)
97
+ assert_equal( [:x, :x], prc2.call(:x, :y) )
98
+ end
99
+ def test_trans__capped_by_args
100
+ prc = lambda {|*a| a }
101
+ prc2 = prc.trans(2, 1, 0)
102
+ assert_equal( [nil, :b], prc2.call(:a, :b) )
103
+ end
104
+ def test_trans__capped_by_indices
105
+ prc = lambda {|*a| a }
106
+ prc2 = prc.trans(1, 0)
107
+ assert_equal( [:b, :a], prc2.call(:a, :b, :c) )
108
+ end
109
+ # arity: :min (default, same as above tests)
110
+ def test_trans__arity_min_explicit
111
+ prc = lambda {|*a| a }
112
+ prc2 = prc.trans(1, 0, arity: :min)
113
+ assert_equal( [:b, :a], prc2.call(:a, :b, :c) )
114
+ end
115
+ # arity: :indices
116
+ def test_trans__arity_indices__args_short
117
+ prc = lambda {|*a| a }
118
+ prc2 = prc.trans(2, 0, 1, arity: :indices)
119
+ assert_equal( [nil, :a, :b], prc2.call(:a, :b) )
120
+ end
121
+ def test_trans__arity_indices__args_long
122
+ prc = lambda {|*a| a }
123
+ prc2 = prc.trans(1, 0, arity: :indices)
124
+ assert_equal( [:b, :a], prc2.call(:a, :b, :c) )
125
+ end
126
+ # arity: :arguments
127
+ def test_trans__arity_arguments__args_short
128
+ prc = lambda {|*a| a }
129
+ prc2 = prc.trans(2, 0, 1, arity: :arguments)
130
+ assert_equal( [nil, :a], prc2.call(:a, :b) )
131
+ end
132
+ def test_trans__arity_arguments__args_long
133
+ prc = lambda {|*a| a }
134
+ prc2 = prc.trans(1, 0, arity: :arguments)
135
+ assert_equal( [:b, :a, :c], prc2.call(:a, :b, :c) )
136
+ end
137
+ # arity: :max
138
+ def test_trans__arity_max__args_short
139
+ prc = lambda {|*a| a }
140
+ prc2 = prc.trans(2, 0, 1, arity: :max)
141
+ assert_equal( [nil, :a, :b], prc2.call(:a, :b) )
142
+ end
143
+ def test_trans__arity_max__args_long
144
+ prc = lambda {|*a| a }
145
+ prc2 = prc.trans(1, 0, arity: :max)
146
+ assert_equal( [:b, :a, :c], prc2.call(:a, :b, :c) )
147
+ end
148
+ def test_trans__arity_max__equal_length
149
+ prc = lambda {|*a| a }
150
+ prc2 = prc.trans(2, 0, 1, arity: :max)
151
+ assert_equal( [:c, :a, :b], prc2.call(:a, :b, :c) )
152
+ end
153
+ # invalid arity
154
+ def test_trans__arity_invalid
155
+ prc = lambda {|*a| a }
156
+ assert_raise( ArgumentError ) { prc.trans(0, 1, arity: :bogus) }
157
+ end
158
+ end
159
+
160
+ class Test_functional_zipmap < Test::Unit::TestCase
161
+ def test_zipmap__readme_example
162
+ printer = lambda {|*x| x }
163
+ mapped = printer.zipmap(:upcase, :downcase, :to_sym)
164
+ assert_equal( ['HELLO', 'there', :Everyone], mapped.call('Hello', 'There', 'Everyone') )
165
+ end
166
+ def test_zipmap__nil_passthrough
167
+ prc = lambda {|a, b| [a, b] }
168
+ prc2 = prc.zipmap(nil, :to_s)
169
+ assert_equal( [42, '42'], prc2.call(42, 42) )
170
+ end
171
+ def test_zipmap__callable
172
+ doubler = lambda {|x| x * 2 }
173
+ prc = lambda {|a, b| [a, b] }
174
+ prc2 = prc.zipmap(doubler, nil)
175
+ assert_equal( [10, 5], prc2.call(5, 5) )
176
+ end
177
+ def test_zipmap__type_error
178
+ prc = lambda {|a| a }
179
+ prc2 = prc.zipmap(123)
180
+ assert_raise( TypeError ) { prc2.call('x') }
181
+ end
182
+ end
183
+
184
+ class Test_functional_juxt < Test::Unit::TestCase
185
+ def test_juxt__readme_example
186
+ func = Proc.juxt :upcase, :downcase, :length
187
+ assert_equal( ['ABC', 'abc', 3], func.call('AbC') )
188
+ end
189
+ def test_juxt__with_procs
190
+ doubler = lambda {|x| x * 2 }
191
+ negator = lambda {|x| -x }
192
+ func = Proc.juxt(doubler, negator)
193
+ assert_equal( [10, -5], func.call(5) )
194
+ end
195
+ end
196
+
197
+ class Test_functional_identity < Test::Unit::TestCase
198
+ def test_identity__returns_same_object
199
+ func = Proc.identity
200
+ obj = Object.new
201
+ assert_same( obj, func.call(obj) )
202
+ end
203
+ def test_identity__with_values
204
+ func = Proc.identity
205
+ assert_equal( 42, func.call(42) )
206
+ assert_equal( 'hello', func.call('hello') )
207
+ assert_nil( func.call(nil) )
208
+ end
209
+ end
210
+
211
+ class Test_functional_const < Test::Unit::TestCase
212
+ def test_const__returns_constant
213
+ obj = Object.new
214
+ func = Proc.const(obj)
215
+ assert_same( obj, func.call )
216
+ assert_same( obj, func.call(1, 2, 3) )
217
+ end
218
+ def test_const__same_object_mutations_stick
219
+ ary = [1, 2, 3]
220
+ func = Proc.const(ary)
221
+ func.call.push(4)
222
+ assert_equal( [1, 2, 3, 4], func.call )
223
+ end
224
+ end
225
+
226
+ class Test_functional_unfold < Test::Unit::TestCase
227
+ def test_unfold__counting
228
+ enum = Enumerator.unfold(1) {|n| [n, n + 1] }
229
+ assert_equal( [1, 2, 3, 4, 5], enum.take(5) )
230
+ end
231
+ def test_unfold__fibonacci
232
+ enum = Enumerator.unfold([0, 1]) {|(a, b)| [a, [b, a + b]] }
233
+ assert_equal( [0, 1, 1, 2, 3], enum.take(5) )
234
+ end
235
+ def test_unfold__termination
236
+ enum = Enumerator.unfold(1) {|n| n <= 3 ? [n, n + 1] : nil }
237
+ assert_equal( [1, 2, 3], enum.to_a )
238
+ end
239
+ def test_unfold__no_block
240
+ assert_raise( ArgumentError ) { Enumerator.unfold(1) }
241
+ end
242
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mug
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Kerwin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-22 00:00:00.000000000 Z
11
+ date: 2026-05-01 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  == MUG: Matty's Ultimate Gem
@@ -45,6 +45,7 @@ files:
45
45
  - lib/mug/enumerable/counts.rb
46
46
  - lib/mug/enumerable/hash-like.rb
47
47
  - lib/mug/fragile-method-chain.rb
48
+ - lib/mug/functional.rb
48
49
  - lib/mug/hash.rb
49
50
  - lib/mug/hash/fetch-assign.rb
50
51
  - lib/mug/hash/map.rb
@@ -92,6 +93,7 @@ files:
92
93
  - test/test-enumerable-chain.rb
93
94
  - test/test-enumerable-hash-like.rb
94
95
  - test/test-fragile-method-chain.rb
96
+ - test/test-functional.rb
95
97
  - test/test-hashfetchassign.rb
96
98
  - test/test-hashmap.rb
97
99
  - test/test-hashmerge.rb
@@ -157,6 +159,7 @@ test_files:
157
159
  - test/test-enumerable-chain.rb
158
160
  - test/test-enumerable-hash-like.rb
159
161
  - test/test-fragile-method-chain.rb
162
+ - test/test-functional.rb
160
163
  - test/test-hashfetchassign.rb
161
164
  - test/test-hashmap.rb
162
165
  - test/test-hashmerge.rb