fun-ruby 0.0.1

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.
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../fun_ruby"
4
+ require_relative "../function"
5
+
6
+ module FunRuby
7
+ module Common
8
+ # Helpers for shorting internal implementations
9
+ module Helpers
10
+ private
11
+
12
+ def curry_implementation(method_name, *args)
13
+ F::Function.curry(method("_#{method_name}")).(*args)
14
+ end
15
+ end
16
+ end
17
+
18
+ private_constant :Common
19
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../container"
4
+ require_relative "../container/resolve"
5
+
6
+ module FunRuby
7
+ class Container
8
+ # @private
9
+ class Define
10
+ private_class_method :new
11
+
12
+ # @private
13
+ def self.build(container: FunRuby.container, namespaces: [])
14
+ raise TypeError, "namespaces: should be an array" unless namespaces.is_a?(::Array)
15
+ raise TypeError, "container: should be an instance of #{Container.name}" unless container.is_a?(Container)
16
+
17
+ namespaces = namespaces.map(&:to_s)
18
+ resolve = Resolve.build(
19
+ container: container,
20
+ aliases: (0...namespaces.size).reduce([]) do |combos, index|
21
+ [namespaces[0..index].join(NAMESPACE_SEPARATOR), *combos]
22
+ end
23
+ )
24
+ new(
25
+ container: container,
26
+ namespaces: namespaces,
27
+ resolve: resolve
28
+ )
29
+ end
30
+
31
+ # @private
32
+ def initialize(container:, namespaces:, resolve:)
33
+ @container = container
34
+ @namespaces = namespaces
35
+ @resolve = resolve
36
+ end
37
+
38
+ # @private
39
+ def namespace(namespace, &block)
40
+ self.class.build(container: container, namespaces: [*namespaces, namespace]).tap do |define|
41
+ define.instance_exec(&block)
42
+ end
43
+ end
44
+
45
+ # @private
46
+ def call(&block)
47
+ instance_exec(&block)
48
+ end
49
+
50
+ # @private
51
+ def function(key, &block)
52
+ if block.nil?
53
+ resolve.(key)
54
+ else
55
+ full_key = [*namespaces, key].join(NAMESPACE_SEPARATOR)
56
+ container.define(full_key, &block)
57
+ end
58
+ end
59
+ alias f function
60
+
61
+ private
62
+
63
+ attr_reader :container, :namespaces, :resolve
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "resolve"
4
+
5
+ module FunRuby
6
+ class Container
7
+ # @private
8
+ module Mixin
9
+ # @private
10
+ def self.build(aliases:)
11
+ mixin = Module.new
12
+ mixin.send(:define_method, :_resolve) { Resolve.build(aliases: aliases) }
13
+ mixin.send(:include, self)
14
+ mixin
15
+ end
16
+
17
+ private
18
+
19
+ def _resolve
20
+ @_resolve ||= Resolve.build
21
+ end
22
+
23
+ def f(key)
24
+ _resolve.(key)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FunRuby
4
+ class Container
5
+ # @private
6
+ class Resolve
7
+ private_class_method :new
8
+
9
+ # @private
10
+ def self.build(aliases: [], container: FunRuby.container)
11
+ formatted = aliases.each_with_object({}) do |key, namespace|
12
+ if key.is_a?(::Hash)
13
+ namespace.merge!(key.map { |k, v| [k, v].map(&:to_s) }.to_h)
14
+ else
15
+ namespace[key.to_s] = nil
16
+ end
17
+ end
18
+ new(aliases: formatted.to_a.reverse.to_h, container: container)
19
+ end
20
+
21
+ # @private
22
+ def initialize(aliases:, container:)
23
+ @aliases = aliases
24
+ @container = container
25
+ end
26
+
27
+ # @private
28
+ def call(key)
29
+ key = key.to_s
30
+ aliases.each do |(namespace, shortcut)|
31
+ full_key = build_full_key(namespace, shortcut, key)
32
+ begin
33
+ return container.fetch(full_key)
34
+ rescue KeyError
35
+ next
36
+ end
37
+ end
38
+
39
+ container.fetch(key) do
40
+ raise KeyError, "key #{key.inspect} has not been registered"
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def build_full_key(namespace, shortcut, key)
47
+ if shortcut.nil?
48
+ [namespace, key].join(NAMESPACE_SEPARATOR)
49
+ else
50
+ key.gsub(/(?<=\A|\.)#{shortcut}(?=\z|\.)/, namespace)
51
+ end
52
+ end
53
+
54
+ attr_reader :aliases, :container
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "container/mixin"
4
+
5
+ module FunRuby
6
+ # @private
7
+ class Container
8
+ NAMESPACE_SEPARATOR = "."
9
+ NOT_EVALUATED = Object.new.freeze
10
+
11
+ # @private
12
+ def initialize
13
+ @storage = {}
14
+ @mutex = Mutex.new
15
+ end
16
+
17
+ # @private
18
+ def define(key, &block)
19
+ key = key.to_s
20
+
21
+ raise KeyError, "#{key.inspect} is already defined" if storage.key?(key)
22
+ raise TypeError, "block should be given" unless block_given?
23
+
24
+ storage[key] = init_meta(block)
25
+ end
26
+
27
+ # @private
28
+ def fetch(key)
29
+ key = key.to_s
30
+ meta = storage.fetch(key)
31
+ value = storage[key].fetch(:value)
32
+ return value unless value.equal?(NOT_EVALUATED)
33
+
34
+ meta.fetch(:definition).().tap do |evaluated|
35
+ storage[key][:value] = evaluated
36
+ end
37
+ end
38
+
39
+ # @private
40
+ def import(*aliases)
41
+ Mixin.build(aliases: aliases)
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :storage, :mutex
47
+
48
+ def init_meta(definition)
49
+ {
50
+ definition: definition,
51
+ value: NOT_EVALUATED
52
+ }
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "common/helpers"
4
+
5
+ module FunRuby
6
+ # Module containing methods for enumerables
7
+ module Enum
8
+ include Common::Helpers
9
+
10
+ extend self
11
+
12
+ # Returns a boolean which represents if all results
13
+ # of applying a function to each element are truthy
14
+ #
15
+ # @since 0.1.0
16
+ #
17
+ # @param function [#call/1]
18
+ # @param enumerable [#to_enum]
19
+ #
20
+ # @return [Boolean]
21
+ #
22
+ # @example Basic returning true
23
+ # F::Enum.all?(->(x) { x % 2 == 0 }, [2, 4, 6]) #=> true
24
+ #
25
+ # @example Basic returning false
26
+ # F::Enum.all?(->(x) { x % 2 == 0 }, [2, 5, 6]) #=> false
27
+ #
28
+ # @example Curried
29
+ # curried = F::Enum.all?
30
+ # curried.(->(x) { x % 2 == 0 }).([1, 2, 3]) #=> false
31
+ #
32
+ # @example Curried with placeholder
33
+ # curried = F::Enum.all?(F._, [2, 4, 6])
34
+ # curried.(->(x) { x % 2 == 0 }) # => true
35
+ def all?(function = F._, enumerable = F._)
36
+ curry_implementation(:all?, function, enumerable)
37
+ end
38
+
39
+ # Applies a function to each element of an enumerable and
40
+ # returns the initial enumerable
41
+ #
42
+ # @since 0.1.0
43
+ #
44
+ # @param function [#call/1]
45
+ # @param enumerable [#to_enum]
46
+ #
47
+ # @return [::Array] the passed enumerable
48
+ #
49
+ # @example Basic
50
+ # F::Enum.each(->(x) { puts x }, [1, 2, 3]) #=> [1, 2, 3]
51
+ #
52
+ # @example Curried
53
+ # curried = F::Enum.each
54
+ # curried.(->(x) { puts x }).([1, 2, 3]) #=> [1, 2, 3]
55
+ #
56
+ # @example Curried with placeholder
57
+ # curried = F::Enum.each(F._, [1, 2, 3])
58
+ # curried.(->(x) { puts x }) #=> [1, 2, 3]
59
+ def each(function = F._, enumerable = F._)
60
+ curry_implementation(:each, function, enumerable)
61
+ end
62
+
63
+ # Returns a new enumerable with new values calculated
64
+ # by applying a function to each element of a passed enumerable
65
+ #
66
+ # @since 0.1.0
67
+ #
68
+ # @param function [#call/1]
69
+ # @param enumerable [#to_enum]
70
+ #
71
+ # @return [::Array] a new enumerable with calculated values
72
+ #
73
+ # @example Basic
74
+ # F::Enum.map(->(x) { x * 2 }, [1, 2, 3]) #=> [2, 4, 6]
75
+ #
76
+ # @example Curried
77
+ # curried = F::Enum.map
78
+ # curried.(->(x) { x * 2 }).([1, 2, 3]) #=> [2, 4, 6]
79
+ #
80
+ # @example Curried with placeholder
81
+ # curried = F::Enum.map(F._, [1, 2, 3])
82
+ # curried.(->(x) { x * 2 }) # => [2, 4, 6]
83
+ def map(function = F._, enumerable = F._)
84
+ curry_implementation(:map, function, enumerable)
85
+ end
86
+
87
+ # Returns a new enumerable containing only these elements
88
+ # results of calling a function on them are truthy
89
+ #
90
+ # @since 0.1.0
91
+ #
92
+ # @param function [#call/1]
93
+ # @param enumerable [#to_enum]
94
+ #
95
+ # @return [::Array] a new enumerable with selected values
96
+ #
97
+ # @example Basic
98
+ # F::Enum.select(->(x) { x % 2 == 0 }, [1, 2, 3, 4, 5]) #=> [2, 4]
99
+ #
100
+ # @example Curried
101
+ # curried = F::Enum.select
102
+ # curried.(->(x) { x % 2 == 0 }).( [1, 2, 3, 4, 5]) #=> [2, 4]
103
+ #
104
+ # @example Curried with placeholder
105
+ # curried = F::Enum.select(F._, [1, 2, 3, 4, 5])
106
+ # curried.(->(x) { x % 2 == 0 }) #=> [2, 4]
107
+ def select(function = F._, enumerable = F._)
108
+ curry_implementation(:select, function, enumerable)
109
+ end
110
+
111
+ # Returns a single item by iterating through the list, successively
112
+ # calling the iterator function and passing it an accumulator value
113
+ # and the current value from the array, and then passing the result
114
+ # to the next call.
115
+ #
116
+ # @since 0.1.0
117
+ #
118
+ # @param function [#call/2] ->(accumulator, element) {}
119
+ # @param accumulator Object
120
+ # @param enumerable [#to_enum]
121
+ #
122
+ # @return Object a result of reducing the enumerable
123
+ #
124
+ # @example Basic: sum of all elements
125
+ # F::Enum.reduce(->(acc, x) { acc + x }, 0, [1, 2, 3]) #=> 6
126
+ #
127
+ # @example Basic: subtraction of all elements
128
+ # F::Enum.reduce(->(acc, x) { acc - x }, 0, [1, 2, 3]) #=> -6
129
+ #
130
+ # @example Basic: histogram
131
+ # F::Enum.reduce(
132
+ # ->(acc, x) { acc[x] += 1; acc },
133
+ # Hash.new(0),
134
+ # [1, 1, 1, 2, 2, 3]
135
+ # ) #=> { 1 => 3, 2 => 2, 3 => 1 }
136
+ #
137
+ # @example Curried: sum
138
+ # curried = F::Enum.reduce
139
+ # curried.(->(acc, x) { acc + x }).(0).([1, 2, 3]) #=> 6
140
+ #
141
+ # @example Curried: sum with placeholder for function
142
+ # curried = F::Enum.reduce(F._, 0, [1, 2, 3])
143
+ # curried.(->(acc, x) { acc + x }) #=> 6
144
+ #
145
+ # @example Curried: sum with placeholder for accumulator
146
+ # curried = F::Enum.reduce(->(acc, x) { acc + x }, F._, [1, 2, 3])
147
+ # curried.(0) #=> 6
148
+ #
149
+ # @example Curried: sum with placeholder for function and accumulator
150
+ # curried = F::Enum.reduce(F._, F._, [1, 2, 3])
151
+ # curried.(->(acc, x) { acc + x }, 0) # => 6
152
+ # curried.(->(acc, x) { acc + x }).(0) # => 6
153
+ def reduce(function = F._, accumulator = F._, enumerable = F._)
154
+ curry_implementation(:reduce, function, accumulator, enumerable)
155
+ end
156
+
157
+ # Goes through a given enumerable and count the amount of elements
158
+ # for which a passed function returned true
159
+ #
160
+ # @since 0.1.0
161
+ #
162
+ # @param function [#call/1] ->(item) { returns Boolean }
163
+ # @param enumerable [#to_enum]
164
+ #
165
+ # @return [Integer]
166
+ #
167
+ # @example Basic: count odd elements
168
+ # F::Enum.count(->(x) { x % 2 == 1 }, [1, 2, 3]) #=> 2
169
+ #
170
+ # @example Curried: count odd elements
171
+ # curried = F::Enum.count
172
+ # curried.(->(x) { x % 2 == 1 }).([1, 2, 3]) #=> 2
173
+ #
174
+ # @example Curried with placeholder: count odd elements
175
+ # curried = F::Enum.count(F._, [1, 2, 3])
176
+ # curried.(->(x) { x % 2 == 1 }) #=> 2
177
+ #
178
+ # curried = F::Enum.count(->(x) { x % 2 == 1 }, F._)
179
+ # curried.([1, 2, 3]) #=> 2
180
+ def count(function = F._, enumerable = F._)
181
+ curry_implementation(:count, function, enumerable)
182
+ end
183
+
184
+ private
185
+
186
+ def _all?(function, enumerable)
187
+ _enum(enumerable).all?(&function)
188
+ end
189
+
190
+ def _each(function, enumerable)
191
+ _enum(enumerable).each(&function)
192
+ end
193
+
194
+ def _map(function, enumerable)
195
+ _enum(enumerable).map(&function)
196
+ end
197
+
198
+ def _select(function, enumerable)
199
+ _enum(enumerable).select(&function)
200
+ end
201
+
202
+ def _reduce(function, accumulator, enumerable)
203
+ _enum(enumerable).reduce(accumulator, &function)
204
+ end
205
+
206
+ def _count(function, enumerable)
207
+ _enum(enumerable).count(&function)
208
+ end
209
+
210
+ def _enum(enumerable)
211
+ enumerable.to_enum
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FunRuby
4
+ # Module containing methods for files
5
+ module File
6
+ include Common::Helpers
7
+
8
+ extend self
9
+
10
+ # Writes a given content to a given file
11
+ #
12
+ # @since 0.1.0
13
+ #
14
+ # @param filepath [::String]
15
+ # @param content [::String]
16
+ #
17
+ # @example Basics
18
+ # tmp = Tempfile.new("file.txt")
19
+ # F::File.write(tmp.path, "abc")
20
+ # File.read(tmp.path) #=> "abc"
21
+ # tmp.unlink
22
+ def write(filepath = F._, content = F._)
23
+ curry_implementation(:write, filepath, content)
24
+ end
25
+
26
+ private
27
+
28
+ def _write(filepath, content)
29
+ ::File.open(filepath, "w") { |file| file.write(content) }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "common/helpers"
4
+
5
+ # Top-level
6
+ module FunRuby
7
+ # Module for useful manipulations with functions
8
+ module Function
9
+ extend self
10
+
11
+ # Performs right-to-left function composition
12
+ #
13
+ # @since 0.1.0
14
+ #
15
+ # @param required [#call]
16
+ # @param *functions [#call]
17
+ #
18
+ # @return [#call]
19
+ #
20
+ # @example Basic: one function
21
+ # plus_2 = ->(x) { x + 2 }
22
+ #
23
+ # curried = F::Function.compose(plus_2)
24
+ # curried.(2) # => 4
25
+ #
26
+ # @example Basic: two functions
27
+ # plus_2 = ->(x) { x + 2 }
28
+ # multiply_3 = ->(x) { x * 3 }
29
+ #
30
+ # # multiplying -> adding
31
+ # curried = F::Function.compose(plus_2, multiply_3)
32
+ #
33
+ # curried.(2) # => 8
34
+ #
35
+ # # adding -> multiplying
36
+ # curried = F::Function.compose(multiply_3, plus_2)
37
+ #
38
+ # curried.(2) # => 12
39
+ #
40
+ # @example Basic: three functions
41
+ # plus_2 = ->(x) { x + 2 }
42
+ # multiply_3 = ->(x) { x * 3 }
43
+ # mod_4 = ->(x) { x % 4 }
44
+ #
45
+ # # multiplying -> adding -> mod
46
+ # curried = F::Function.compose(mod_4, plus_2, multiply_3)
47
+ # curried.(4) # => 2
48
+ def compose(required = F._, *functions)
49
+ curry(method(:_compose)).(required, *functions)
50
+ end
51
+
52
+ # Performs left-to-right function composition
53
+ #
54
+ # @since 0.1.0
55
+ #
56
+ # @param required [#call]
57
+ # @param *functions [#call]
58
+ #
59
+ # @return [#call]
60
+ #
61
+ # @example Basic: one function
62
+ # plus_2 = ->(x) { x + 2 }
63
+ #
64
+ # curried = F::Function.pipe(plus_2)
65
+ # curried.(2) # => 4
66
+ #
67
+ # @example Basic: two functions
68
+ # plus_2 = ->(x) { x + 2 }
69
+ # multiply_3 = ->(x) { x * 3 }
70
+ #
71
+ # # multiplying -> adding
72
+ # curried = F::Function.pipe(multiply_3, plus_2)
73
+ #
74
+ # curried.(2) # => 8
75
+ #
76
+ # # adding -> multiplying
77
+ # curried = F::Function.pipe(plus_2, multiply_3)
78
+ #
79
+ # curried.(2) # => 12
80
+ #
81
+ # @example Basic: three functions
82
+ # plus_2 = ->(x) { x + 2 }
83
+ # multiply_3 = ->(x) { x * 3 }
84
+ # mod_4 = ->(x) { x % 4 }
85
+ #
86
+ # # multiplying -> adding -> mod
87
+ # curried = F::Function.pipe(multiply_3, plus_2, mod_4)
88
+ # curried.(4) # => 2
89
+ def pipe(required = F._, *functions)
90
+ curry(method(:_pipe)).(required, *functions)
91
+ end
92
+
93
+ # @example
94
+ # build_ary = ->(a, b, c) { [a, b, c] }
95
+ # curry = F::Function.curry
96
+ # curried = curry.(build_ary)
97
+ #
98
+ # curried.(1, 2, 3) # => [1, 2, 3]
99
+ # curried.(1).(2).(3) # => [1, 2, 3]
100
+ # curried.(F._, 2).(F._, 3).(1) # => [1, 2, 3]
101
+ # curried.(F._, 2, F._).(1).(3) # => [1, 2, 3]
102
+ # curried.(F._, F._, 3).(F._, 2).(1) # => [1, 2, 3]
103
+ # curried.(F._, 2, F._).(1, 3) # => [1, 2, 3]
104
+ # curried.(F._, 2, F._).(1, 3) # => [1, 2, 3]
105
+ def curry(function = F._)
106
+ handling_placeholders(method(:_curry).curry, [function])
107
+ end
108
+
109
+ private
110
+
111
+ def _pipe(required, *functions)
112
+ functions = [required, *functions]
113
+ functions.reduce do |composed, function|
114
+ ->(*args) { function.(composed.(*args)) }
115
+ end
116
+ end
117
+
118
+ def _compose(required, *functions)
119
+ _pipe(*functions, required)
120
+ end
121
+
122
+ def _curry(function)
123
+ handling_placeholders(function.curry)
124
+ end
125
+
126
+ def handling_placeholders(function, processed_args = [], *new_args)
127
+ processed_args, _ = new_args.reduce([processed_args, 0], &method(:apply_arg))
128
+ values, with_placeholders = bisect_args(processed_args)
129
+ result = function.(*values)
130
+ return result unless result.respond_to?(:call)
131
+
132
+ ->(*args) { handling_placeholders(result, with_placeholders, *args) }
133
+ end
134
+
135
+ def apply_arg((processed_args, shift), new_arg)
136
+ processed_args = processed_args.dup
137
+ placeholder_index = processed_args[shift..-1].to_a.index(F._)
138
+ if placeholder_index
139
+ real_position = placeholder_index + shift
140
+ processed_args[real_position] = new_arg
141
+ [processed_args, real_position + 1]
142
+ else
143
+ processed_args << new_arg
144
+ [processed_args, processed_args.size]
145
+ end
146
+ end
147
+
148
+ def bisect_args(args)
149
+ index = args.index(F._)
150
+ return [args, []] if index.nil?
151
+
152
+ values = args[0...index]
153
+ placeholders = args[index..-1] || []
154
+ [values, placeholders]
155
+ end
156
+ end
157
+ end