fun-ruby 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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