invokable 0.5.1 → 0.7.2
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 +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +54 -4
- data/Gemfile.lock +1 -1
- data/README.md +88 -75
- data/_config.yml +1 -0
- data/lib/core_ext.rb +15 -0
- data/lib/invokable.rb +82 -9
- data/lib/invokable/array.rb +11 -8
- data/lib/invokable/closure.rb +114 -0
- data/lib/invokable/command.rb +5 -104
- data/lib/invokable/compose.rb +14 -8
- data/lib/invokable/core.rb +30 -14
- data/lib/invokable/data.rb +1 -0
- data/lib/invokable/hash.rb +11 -7
- data/lib/invokable/helpers.rb +141 -0
- data/lib/invokable/set.rb +11 -7
- data/lib/invokable/version.rb +1 -1
- metadata +6 -4
- data/lib/invokable/method.rb +0 -5
- data/lib/invokable/proc.rb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ff6115c057fbcfffd52d6037a190e9a36b23018f1c2b0745b2b9c7bb333cb86c
|
4
|
+
data.tar.gz: 4ebd7bdb25bdfbde7a762d333b9215f8e13909091fab11c1f048f52447e19b3c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0d021ad9461294d8ee384d2302d25012b148e614c0deab1025d77da3a53355bbe62d326d9f29f15ed0685113523a577b13eef7a4d30ab81e323efaa59cecfb0a
|
7
|
+
data.tar.gz: 5469ed8b60c2d81cc89ad7f0a6debabf76b4f55e944f110365c2ed41db25379833f970acc288426ce61b123fffb6f3964cacc2d7ccc4689f9b7d3011b1654c02
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,11 +1,61 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
-
## 0.
|
3
|
+
## 0.7.2
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
Variable arity invoker methods are handled properly. Variable arity initializers will result in a more meaningful exception being raised.
|
6
|
+
|
7
|
+
## 0.7.1
|
8
|
+
|
9
|
+
### Invokable::call
|
10
|
+
|
11
|
+
When all the arguments of the initializer are passed and the instance method is zero arity the instance method will be called.
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
class Greeter
|
15
|
+
def initialize(name)
|
16
|
+
@name = name
|
17
|
+
end
|
18
|
+
|
19
|
+
def call
|
20
|
+
"Hello #{name}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
Greeter.call("Jane") # => "Hello Jane"
|
25
|
+
Greeter.call("John") # => #<Greeter ...> (before 0.7.1)
|
26
|
+
```
|
27
|
+
|
28
|
+
## 0.7.0
|
29
|
+
|
30
|
+
- Added helper methods `juxtapose`, `knit`, `compose`, `identity`, `always`, `guarded`, `partial`, and `coerce`
|
31
|
+
that can be used as module functions on `Invokable` or in classes that mixin the `Invokable` module.
|
32
|
+
|
33
|
+
- For classes whose initialize method takes no arguments, when the class method `call` is called it will
|
34
|
+
initialize the class and call it's `call` method.
|
35
|
+
|
36
|
+
- `[]` and `===` are added to classed that mixin `Invokable` for better `Proc` compatibility.
|
37
|
+
|
38
|
+
- `Array`, `Hash` and `Set` patches no longer include the invokable methods, they simply add `to_proc`.
|
39
|
+
|
40
|
+
- When `invokable/data` is required the array patch is also loaded.
|
41
|
+
|
42
|
+
- All the methods that take an invokable will "coerce" the invokable by simply returning it if it implements `call`
|
43
|
+
or coercing it into a proc if it implements `to_proc`.
|
44
|
+
|
45
|
+
## 0.6.0
|
46
|
+
|
47
|
+
- `Invokable::Closure` deprecated comparable behavior has been added to `Invokable` itself.
|
48
|
+
|
49
|
+
## 0.5.2
|
50
|
+
|
51
|
+
- `Invokable::Command` deprecated in favor of `Invokable::Closure`.
|
8
52
|
|
9
53
|
## 0.5.0
|
10
54
|
|
11
55
|
- Added `Invokable::Command` and `Invokable::Core#arity`
|
56
|
+
|
57
|
+
## 0.4.2
|
58
|
+
|
59
|
+
- `invokable/array` is no longer loaded with `invokable/data`.
|
60
|
+
This created a bit of havok in a few places. Including breaking
|
61
|
+
puma bootup in Rails 5.2.4.1.
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -3,85 +3,131 @@
|
|
3
3
|
|
4
4
|
# Invokable
|
5
5
|
|
6
|
-
Objects are functions! Treat any Object
|
6
|
+
Objects are functions! Treat any Object or Class as a Proc (like Enumerable but for Procs)
|
7
7
|
|
8
8
|
## Synopsis
|
9
9
|
|
10
|
+
Objects that enclose state, can be treated as automatically curried functions.
|
11
|
+
|
10
12
|
```ruby
|
11
13
|
require 'invokable'
|
12
|
-
require 'invokable/hash'
|
13
14
|
|
14
|
-
|
15
|
-
|
15
|
+
class TwitterPoster
|
16
|
+
include Invokable
|
17
|
+
|
18
|
+
def initialize(model)
|
19
|
+
@model = model
|
20
|
+
end
|
21
|
+
|
22
|
+
def call(user)
|
23
|
+
# do the dirt
|
24
|
+
...
|
25
|
+
TwitterStatus.new(user, data)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
TwitterPoster.call(Model.find(1)) # => #<TwitterPoster ...>
|
30
|
+
TwitterPoster.call(Model.find(1), current_user) # => #<TwitterStatus ...>
|
31
|
+
|
32
|
+
# both the class and it's instances can be used anywhere Procs are.
|
33
|
+
|
34
|
+
Model.where(created_at: Date.today).map(&TwitterPoster) # => [#<TwitterPoster ...>, ...]
|
16
35
|
```
|
17
36
|
|
37
|
+
Use `memoize`, `<<` and `>>` for composition and other methods on Proc on your command objects
|
38
|
+
|
18
39
|
```ruby
|
19
|
-
|
20
|
-
|
40
|
+
# app/queries/filter_records.rb
|
41
|
+
class FilterRecords
|
42
|
+
include Invokable
|
21
43
|
|
22
|
-
|
23
|
-
|
44
|
+
def initialize(params)
|
45
|
+
@params = params
|
46
|
+
end
|
47
|
+
|
48
|
+
def call(records)
|
49
|
+
# do filtering return records
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# app/queries/sort_records.rb
|
54
|
+
class SortRecords
|
55
|
+
include Invokable
|
56
|
+
|
57
|
+
def initialize(params)
|
58
|
+
@params = params
|
59
|
+
end
|
60
|
+
|
61
|
+
def call(records)
|
62
|
+
# do sorting return records
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
sort_and_filter = SortRecords.call(params) << FilterRecords.call(params)
|
67
|
+
sort_and_filter.call(records) # => sorted and filtered records
|
24
68
|
```
|
25
69
|
|
70
|
+
Helper methods that can be used with any object that responds to `call` or `to_proc`
|
71
|
+
|
26
72
|
```ruby
|
27
|
-
|
28
|
-
require 'invokable/set'
|
73
|
+
Invokable.juxtapose(:sum, -> (xs) { xs.reduce(:*) }, :min, :max).([3, 4, 6]) # => [13, 72, 3, 6]
|
29
74
|
|
30
|
-
|
31
|
-
[1, 2, 3, 4].select(&favorite_numbers) # => [3]
|
75
|
+
Invokable.knit(:upcase, :downcase).(['FoO', 'BaR']) # => ["FOO", "bar"]
|
32
76
|
```
|
33
77
|
|
34
|
-
|
35
|
-
# service objects
|
36
|
-
require 'invokable'
|
78
|
+
They are also mixed into any class that includes the module
|
37
79
|
|
38
|
-
|
80
|
+
```ruby
|
81
|
+
class Transformer
|
39
82
|
include Invokable
|
40
83
|
|
41
|
-
def call(
|
42
|
-
|
84
|
+
def call(array)
|
85
|
+
array.map(&juxtapose(identity, compose(:to_s, :upcase))).to_h
|
43
86
|
end
|
44
87
|
end
|
45
88
|
|
46
|
-
|
47
|
-
User.all.map(&data_for_user)
|
89
|
+
Transformer.call([:a, :b, :c, :d]) # => {:a => "A", :b => "B", :c => "C", :d => "D"}
|
48
90
|
```
|
91
|
+
|
92
|
+
Hashes can be treated as functions of their keys
|
93
|
+
|
49
94
|
```ruby
|
50
|
-
# command objects that enclose state, can be treated as automatically curried functions.
|
51
95
|
require 'invokable'
|
52
|
-
require 'invokable/
|
96
|
+
require 'invokable/hash'
|
53
97
|
|
54
|
-
|
55
|
-
|
98
|
+
number_names = { 1 => "One", 2 => "Two", 3 => "Three" }
|
99
|
+
[1, 2, 3, 4].map(&number_names) # => ["One", "Two", "Three", nil]
|
100
|
+
```
|
56
101
|
|
57
|
-
|
58
|
-
@model = model
|
59
|
-
end
|
102
|
+
Arrays can be treated as functions of their indexes
|
60
103
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
TwitterStatus.new(user, data)
|
65
|
-
end
|
66
|
-
end
|
104
|
+
```ruby
|
105
|
+
require 'invokable'
|
106
|
+
require 'invokable/array'
|
67
107
|
|
68
|
-
|
69
|
-
|
108
|
+
alpha = ('a'..'z').to_a
|
109
|
+
[1, 2, 3, 4].map(&alpha) # => ["b", "c", "d", "e"]
|
110
|
+
```
|
70
111
|
|
71
|
-
|
112
|
+
Sets can be treated as predicates
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
require 'invokable'
|
116
|
+
require 'invokable/set'
|
72
117
|
|
73
|
-
|
118
|
+
favorite_numbers = Set[3, Math::PI]
|
119
|
+
[1, 2, 3, 4].select(&favorite_numbers) # => [3]
|
74
120
|
```
|
75
121
|
|
76
|
-
Use as much or a little as you need
|
122
|
+
Use as much or a little as you need
|
77
123
|
|
78
124
|
```ruby
|
79
125
|
require 'invokable' # loads Invokable module
|
80
|
-
require 'invokable/
|
126
|
+
require 'invokable/helpers' # loads Invokable::Helpers module
|
81
127
|
require 'invokable/hash' # loads hash patch
|
82
128
|
require 'invokable/array' # loads array patch
|
83
129
|
require 'invokable/set' # loads set patch
|
84
|
-
require 'invokable/data' # loads hash and
|
130
|
+
require 'invokable/data' # loads hash, set and array patches
|
85
131
|
```
|
86
132
|
|
87
133
|
## Why?
|
@@ -106,38 +152,9 @@ Or install it yourself as:
|
|
106
152
|
|
107
153
|
> gem install invokable
|
108
154
|
|
109
|
-
## API
|
155
|
+
## API Documentation
|
110
156
|
|
111
|
-
|
112
|
-
|
113
|
-
```ruby
|
114
|
-
hash = { a: 1, b, 2 }
|
115
|
-
hash.class # => Hash
|
116
|
-
hash.to_proc.class # => Proc
|
117
|
-
[:a, :b].map(&hash) # => [1, 2]
|
118
|
-
```
|
119
|
-
|
120
|
-
Convert an object into a proc. When the `Invokable` module is included in a class it will do this by
|
121
|
-
returning a proc that passes it's arguments to the object's `call` method. When `invokable/data` is
|
122
|
-
loaded `Hash#call` is mapped to `Hash#dig`, `Array#call` is mapped to `Array#at`, and `Set#call`
|
123
|
-
is mapped to `Set#include?`.
|
124
|
-
|
125
|
-
### `curry(arity = nil) => Proc`
|
126
|
-
|
127
|
-
Returns a curried proc. If the `arity` is given, it determines the number of arguments.
|
128
|
-
(see [Proc#curry](https://ruby-doc.org/core-2.7.0/Proc.html#method-i-curry)).
|
129
|
-
|
130
|
-
### `memoize => Proc`
|
131
|
-
|
132
|
-
Returns a memoized proc, that is, a proc that caches it's return values by it's arguments.
|
133
|
-
|
134
|
-
### `<<(invokable) => Proc`
|
135
|
-
|
136
|
-
Returns a proc that is a composition of this invokable and the given invokable.
|
137
|
-
|
138
|
-
### `>>(invokable) => Proc`
|
139
|
-
|
140
|
-
Returns a proc that is a composition of this invokable and the given invokable.
|
157
|
+
[https://www.rubydoc.info/gems/invokable](https://www.rubydoc.info/gems/invokable)
|
141
158
|
|
142
159
|
## See Also
|
143
160
|
|
@@ -145,10 +162,6 @@ Returns a proc that is a composition of this invokable and the given invokable.
|
|
145
162
|
- [Clojure](https://clojure.org)
|
146
163
|
- [Arc](http://www.arclanguage.org)
|
147
164
|
|
148
|
-
## TODO
|
149
|
-
|
150
|
-
- benchmark Invokable#to_proc
|
151
|
-
|
152
165
|
## License
|
153
166
|
|
154
167
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/_config.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
#theme: jekyll-theme-minimal
|
data/lib/core_ext.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
if RUBY_VERSION.split('.').take(2).join('.').to_f < 2.6
|
2
|
+
# Add {<<} and {>>} for right and left function composition.
|
3
|
+
#
|
4
|
+
# @note These methods were added to Ruby in version 2.6 so this patch will only be applied when running older versions.
|
5
|
+
class Proc
|
6
|
+
include Invokable::Compose
|
7
|
+
end
|
8
|
+
|
9
|
+
# Add {<<} and {>>} for right and left function composition.
|
10
|
+
#
|
11
|
+
# @note These methods were added to Ruby in version 2.6 so this patch will only be applied when running older versions.
|
12
|
+
class Method
|
13
|
+
include Invokable::Compose
|
14
|
+
end
|
15
|
+
end
|
data/lib/invokable.rb
CHANGED
@@ -1,16 +1,89 @@
|
|
1
1
|
require 'invokable/version'
|
2
2
|
require 'invokable/core'
|
3
|
-
require 'invokable/
|
4
|
-
|
5
|
-
# TODO: make use of Gem::Version
|
6
|
-
if RUBY_VERSION.split('.').take(2).join('.').to_f < 2.6
|
7
|
-
require 'invokable/proc'
|
8
|
-
require 'invokable/method'
|
9
|
-
end
|
3
|
+
require 'invokable/helpers'
|
4
|
+
require 'core_ext'
|
10
5
|
|
6
|
+
# A module that attempts to generalize the notion of a first class function or Procs as they are often called
|
7
|
+
# in Ruby. It enables any class and it's objects to be treated as first-class functions. It also provides helpers
|
8
|
+
# includes helpers that can be used for performing higher-order operations on and object that can be treated
|
9
|
+
# as a function.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# class TwitterPoster
|
13
|
+
# include Invokable
|
14
|
+
#
|
15
|
+
# def initialize(model)
|
16
|
+
# @model = model
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# def call(user)
|
20
|
+
# # do the dirt
|
21
|
+
# ...
|
22
|
+
# TwitterStatus.new(user, data)
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# TwitterPoster.call(Model.find(1)) # => #<TwitterPoster ...>
|
27
|
+
# TwitterPoster.call(Model.find(1), current_user) # => #<TwitterStatus ...>
|
11
28
|
module Invokable
|
29
|
+
extend Invokable::Helpers
|
30
|
+
|
12
31
|
def self.included(base)
|
13
|
-
|
14
|
-
|
32
|
+
INCLUDED_MODULES.each do |mod|
|
33
|
+
base.include(mod)
|
34
|
+
base.extend(mod)
|
35
|
+
end
|
36
|
+
base.extend(ClassMethods)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
INCLUDED_MODULES = [Core, Helpers].freeze
|
42
|
+
|
43
|
+
# The methods that are mixed into any class at the class level that includes {Invokable}.
|
44
|
+
#
|
45
|
+
# @note The module should not be used directly.
|
46
|
+
module ClassMethods
|
47
|
+
# Return the "total" arity of the class (i.e. the arity of the initializer and the arity of the call method)
|
48
|
+
#
|
49
|
+
# @version 0.6.0
|
50
|
+
#
|
51
|
+
# @return [Integer]
|
52
|
+
def arity
|
53
|
+
return initializer_arity + invoker_arity if invoker_arity >= 0
|
54
|
+
|
55
|
+
(initializer_arity + invoker_arity.abs) * -1
|
56
|
+
end
|
57
|
+
|
58
|
+
# Handle automatic currying--will accept either the initializer arity or the total arity of the class. If
|
59
|
+
# the initializer arity is used return a class instance. If the total arity is used instantiate the class
|
60
|
+
# and return the results of the `call` method.
|
61
|
+
#
|
62
|
+
# @version 0.6.0
|
63
|
+
#
|
64
|
+
# @see arity
|
65
|
+
def call(*args)
|
66
|
+
raise ArgumentError, "variable length initializer methods are not supported by Invokable" if initializer_arity < 0
|
67
|
+
|
68
|
+
return new.call if arity.zero?
|
69
|
+
return new(*args).call if args.length == initializer_arity && (invoker_arity.zero? || invoker_arity == -1)
|
70
|
+
return new(*args) if args.length == initializer_arity
|
71
|
+
|
72
|
+
if args.length == arity || invoker_arity < 0 && (args.length - initializer_arity) >= (invoker_arity.abs - 1)
|
73
|
+
init_args = args.slice(0, initializer_arity)
|
74
|
+
call_args = args.slice(initializer_arity, args.length)
|
75
|
+
new(*init_args).call(*call_args)
|
76
|
+
else
|
77
|
+
raise ArgumentError, "wrong number of arguments (given #{args.length}, expected #{initializer_arity} or #{arity})"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def initializer_arity
|
82
|
+
instance_method(:initialize).arity
|
83
|
+
end
|
84
|
+
|
85
|
+
def invoker_arity
|
86
|
+
instance_method(:call).arity
|
87
|
+
end
|
15
88
|
end
|
16
89
|
end
|
data/lib/invokable/array.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
# Extend core Array object by aliasing it's `[]` method as `call`,
|
4
|
-
# and including the `Invokable` module.
|
5
|
-
#
|
6
|
-
# @see https://ruby-doc.org/core-2.7.0/Array.html#method-i-5B-5D Array#[]
|
1
|
+
# Extend core Array by adding {to_proc} which will enable arrays to be treated as functions
|
2
|
+
# of their indexes.
|
7
3
|
class Array
|
8
|
-
|
9
|
-
|
4
|
+
# Return a proc that takes the index of the array and returns the value at that index or nil
|
5
|
+
# if there is no value at the given index.
|
6
|
+
#
|
7
|
+
# @return [Proc]
|
8
|
+
def to_proc
|
9
|
+
lambda do |index|
|
10
|
+
at(index)
|
11
|
+
end
|
12
|
+
end
|
10
13
|
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module Invokable
|
2
|
+
# Treat classes as curried functions
|
3
|
+
#
|
4
|
+
# @version 0.5.2
|
5
|
+
#
|
6
|
+
# @deprecated These features are included in the module {Invokable} by default now.
|
7
|
+
module Closure
|
8
|
+
def self.included(klass)
|
9
|
+
klass.include(Invokable)
|
10
|
+
klass.extend(Invokable::Core)
|
11
|
+
klass.extend(Invokable::Compose)
|
12
|
+
klass.extend(ClassMethods)
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
# Return the "total" arity of the class (i.e. the arity of the initializer and the arity of the call method)
|
17
|
+
#
|
18
|
+
# @version 0.5.0
|
19
|
+
# @see https://ruby-doc.org/core-2.7.1/Proc.html#method-i-arity Proc#arity
|
20
|
+
# @see initializer_arity
|
21
|
+
#
|
22
|
+
# @return [Integer]
|
23
|
+
def arity
|
24
|
+
initializer_arity + instance_method(:call).arity
|
25
|
+
end
|
26
|
+
|
27
|
+
# Return the arity of the initializer
|
28
|
+
#
|
29
|
+
# @version 0.5.0
|
30
|
+
# @see arity
|
31
|
+
#
|
32
|
+
# @return [Integer]
|
33
|
+
def initializer_arity
|
34
|
+
instance_method(:initialize).arity
|
35
|
+
end
|
36
|
+
|
37
|
+
# Handle automatic currying--will accept either the initializer arity or the total arity of the class. If
|
38
|
+
# the initializer arity is used return a class instance. If the total arity is used instantiate the class
|
39
|
+
# and return the results of the `call` method.
|
40
|
+
#
|
41
|
+
# @version 0.5.0
|
42
|
+
# @see arity
|
43
|
+
# @see initializer_arity
|
44
|
+
def call(*args)
|
45
|
+
if args.length == initializer_arity
|
46
|
+
new(*args)
|
47
|
+
elsif args.length == arity
|
48
|
+
init_args = args.slice(0, initializer_arity)
|
49
|
+
call_args = args.slice(initializer_arity, args.length)
|
50
|
+
new(*init_args).call(*call_args)
|
51
|
+
else
|
52
|
+
raise ArgumentError, "wrong number of arguments (given #{args.length}, expected #{initializer_arity} or #{arity})"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Specify any enclosed state with a block or named attributes
|
57
|
+
#
|
58
|
+
# @example
|
59
|
+
# class TwitterPater
|
60
|
+
# include Invokable::Command
|
61
|
+
#
|
62
|
+
# enclose :api_key
|
63
|
+
#
|
64
|
+
# def call(user)
|
65
|
+
# # interact with twitter, return results
|
66
|
+
# end
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# TwitterPater.new(API_KEY).call(User.find(1))
|
70
|
+
# TwitterPater.new(API_KEY).api_key == API_KEY # => true
|
71
|
+
#
|
72
|
+
# class TwitterPater
|
73
|
+
# include Invokable::Command
|
74
|
+
#
|
75
|
+
# enclose do |api_key|
|
76
|
+
# @api_key = api_key
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# def call(user)
|
80
|
+
# # interact with twitter, return results
|
81
|
+
# end
|
82
|
+
# end
|
83
|
+
#
|
84
|
+
# TwitterPater.new(API_KEY).call(User.find(1))
|
85
|
+
# TwitterPater.new(API_KEY).api_key # error 'method' missing
|
86
|
+
def enclose(*names, &block)
|
87
|
+
return define_initializer_with_block(block) unless block.nil?
|
88
|
+
|
89
|
+
define_initializer_with_names(names)
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def define_initializer_with_block(block)
|
95
|
+
@initializer = block
|
96
|
+
define_method :initialize, &block
|
97
|
+
end
|
98
|
+
|
99
|
+
def define_initializer_with_names(names)
|
100
|
+
@initializer_arity = names.length
|
101
|
+
|
102
|
+
names.each do |name|
|
103
|
+
attr_reader name
|
104
|
+
end
|
105
|
+
|
106
|
+
define_method :initialize do |*args|
|
107
|
+
names.each_with_index do |name, i|
|
108
|
+
instance_variable_set(:"@#{name}", args[i])
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
data/lib/invokable/command.rb
CHANGED
@@ -1,116 +1,17 @@
|
|
1
|
+
require_relative 'closure'
|
2
|
+
|
1
3
|
module Invokable
|
2
4
|
# Treat "Command Objects" as curried functions
|
3
5
|
#
|
4
|
-
# @see https://ruby-doc.org/core-2.7.0/Proc.html#method-i-curry Proc#curry
|
5
|
-
#
|
6
6
|
# @version 0.5.0
|
7
|
+
#
|
8
|
+
# @deprecated These features are included in the {Invokable} module by default now.
|
7
9
|
module Command
|
8
10
|
def self.included(klass)
|
9
11
|
klass.include(Invokable)
|
10
12
|
klass.extend(Invokable::Core)
|
11
13
|
klass.extend(Invokable::Compose)
|
12
|
-
klass.extend(ClassMethods)
|
13
|
-
end
|
14
|
-
|
15
|
-
module ClassMethods
|
16
|
-
# Return the "total" arity of the class (i.e. the arity of the initializer and the arity of the call method)
|
17
|
-
#
|
18
|
-
# @version 0.5.0
|
19
|
-
# @see https://ruby-doc.org/core-2.7.1/Proc.html#method-i-arity Proc#arity
|
20
|
-
# @see initializer_arity
|
21
|
-
#
|
22
|
-
# @return [Integer]
|
23
|
-
def arity
|
24
|
-
initializer_arity + instance_method(:call).arity
|
25
|
-
end
|
26
|
-
|
27
|
-
# Return the arity of the initializer
|
28
|
-
#
|
29
|
-
# @version 0.5.0
|
30
|
-
# @see arity
|
31
|
-
#
|
32
|
-
# @return [Integer]
|
33
|
-
def initializer_arity
|
34
|
-
return @initializer_arity if @initializer_arity
|
35
|
-
|
36
|
-
@initializer ? @initializer.arity : 0
|
37
|
-
end
|
38
|
-
|
39
|
-
# Handle automatic currying--will accept either the initializer arity or the total arity of the class. If
|
40
|
-
# the initializer arity is used return a class instance. If the total arity is used instantiate the class
|
41
|
-
# and return the results of the `call` method.
|
42
|
-
#
|
43
|
-
# @version 0.5.0
|
44
|
-
# @see arity
|
45
|
-
# @see initializer_arity
|
46
|
-
def call(*args)
|
47
|
-
if args.length == initializer_arity
|
48
|
-
new(*args)
|
49
|
-
elsif args.length == arity
|
50
|
-
init_args = args.slice(0, initializer_arity)
|
51
|
-
call_args = args.slice(initializer_arity, args.length)
|
52
|
-
new(*init_args).call(*call_args)
|
53
|
-
else
|
54
|
-
raise ArgumentError, "wrong number of arguments (given #{args.length}, expected #{initializer_arity} or #{arity})"
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
# Specify any enclosed state with a block or named attributes
|
59
|
-
#
|
60
|
-
# @example
|
61
|
-
# class TwitterPater
|
62
|
-
# include Invokable::Command
|
63
|
-
#
|
64
|
-
# enclose :api_key
|
65
|
-
#
|
66
|
-
# def call(user)
|
67
|
-
# # interact with twitter, return results
|
68
|
-
# end
|
69
|
-
# end
|
70
|
-
#
|
71
|
-
# TwitterPater.new(API_KEY).call(User.find(1))
|
72
|
-
# TwitterPater.new(API_KEY).api_key == API_KEY # => true
|
73
|
-
#
|
74
|
-
# class TwitterPater
|
75
|
-
# include Invokable::Command
|
76
|
-
#
|
77
|
-
# enclose do |api_key|
|
78
|
-
# @api_key = api_key
|
79
|
-
# end
|
80
|
-
#
|
81
|
-
# def call(user)
|
82
|
-
# # interact with twitter, return results
|
83
|
-
# end
|
84
|
-
# end
|
85
|
-
#
|
86
|
-
# TwitterPater.new(API_KEY).call(User.find(1))
|
87
|
-
# TwitterPater.new(API_KEY).api_key # error 'method' missing
|
88
|
-
def enclose(*names, &block)
|
89
|
-
return define_initializer_with_block(block) unless block.nil?
|
90
|
-
|
91
|
-
define_initializer_with_names(names)
|
92
|
-
end
|
93
|
-
|
94
|
-
private
|
95
|
-
|
96
|
-
def define_initializer_with_block(block)
|
97
|
-
@initializer = block
|
98
|
-
define_method :initialize, &block
|
99
|
-
end
|
100
|
-
|
101
|
-
def define_initializer_with_names(names)
|
102
|
-
@initializer_arity = names.length
|
103
|
-
|
104
|
-
names.each do |name|
|
105
|
-
attr_reader name
|
106
|
-
end
|
107
|
-
|
108
|
-
define_method :initialize do |*args|
|
109
|
-
names.each_with_index do |name, i|
|
110
|
-
instance_variable_set(:"@#{name}", args[i])
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|
14
|
+
klass.extend(Invokable::Closure::ClassMethods)
|
114
15
|
end
|
115
16
|
end
|
116
17
|
end
|
data/lib/invokable/compose.rb
CHANGED
@@ -1,24 +1,30 @@
|
|
1
1
|
module Invokable
|
2
|
+
# The {<<} and {>>} methods for right and left function composition.
|
3
|
+
#
|
4
|
+
# This module is included in the {Invokable} module and the Proc and Method classes
|
5
|
+
# when used with versions of Ruby earlier than 2.6 (when they were added).
|
6
|
+
#
|
7
|
+
# @note This module should not be used directly.
|
2
8
|
module Compose
|
3
|
-
# Return a proc that is the composition of this invokable and the given
|
4
|
-
# The returned proc takes a variable number of arguments, calls
|
9
|
+
# Return a proc that is the composition of this invokable and the given invokable.
|
10
|
+
# The returned proc takes a variable number of arguments, calls invokable with
|
5
11
|
# them then calls this proc with the result.
|
6
12
|
#
|
7
13
|
# @return [Proc]
|
8
14
|
def <<(invokable)
|
9
|
-
|
10
|
-
call(invokable.call(*args))
|
15
|
+
lambda do |*args|
|
16
|
+
call(Invokable.coerce(invokable).call(*args))
|
11
17
|
end
|
12
18
|
end
|
13
19
|
|
14
|
-
# Return a proc that is the composition of this invokable and the given
|
15
|
-
# The returned proc takes a variable number of arguments, calls
|
20
|
+
# Return a proc that is the composition of this invokable and the given invokable.
|
21
|
+
# The returned proc takes a variable number of arguments, calls invokable with
|
16
22
|
# them then calls this proc with the result.
|
17
23
|
#
|
18
24
|
# @return [Proc]
|
19
25
|
def >>(invokable)
|
20
|
-
|
21
|
-
invokable.call(call(*args))
|
26
|
+
lambda do |*args|
|
27
|
+
Invokable.coerce(invokable).call(call(*args))
|
22
28
|
end
|
23
29
|
end
|
24
30
|
end
|
data/lib/invokable/core.rb
CHANGED
@@ -1,26 +1,27 @@
|
|
1
|
+
require_relative 'compose'
|
2
|
+
|
1
3
|
module Invokable
|
4
|
+
# The core methods that are mixed into classes at a class and instance level when they
|
5
|
+
# include {Invokable}.
|
6
|
+
#
|
7
|
+
# @note This module should not be used directly.
|
2
8
|
module Core
|
3
|
-
|
9
|
+
include Compose
|
10
|
+
|
11
|
+
# Return a Proc that forwards it's arguments along to call.
|
4
12
|
#
|
5
|
-
# @see https://ruby-doc.org/core-2.7.0/Proc.html#method-i-call Proc#call
|
6
13
|
# @return [Proc]
|
7
14
|
def to_proc
|
8
|
-
|
9
|
-
|
10
|
-
Proc.new do |*args|
|
11
|
-
call(*args)
|
12
|
-
end
|
13
|
-
else
|
14
|
-
raise "Don't know how to convert #{self.inspect} into a Proc"
|
15
|
+
lambda do |*args|
|
16
|
+
call(*args)
|
15
17
|
end
|
16
18
|
end
|
17
19
|
|
18
|
-
# Return a curried proc. If the optional
|
20
|
+
# Return a curried proc. If the optional arity argument is given, it determines the number of arguments.
|
19
21
|
# A curried proc receives some arguments. If a sufficient number of arguments are supplied, it passes the
|
20
22
|
# supplied arguments to the original proc and returns the result. Otherwise, returns another curried proc
|
21
23
|
# that takes the rest of arguments.
|
22
24
|
#
|
23
|
-
# @see https://ruby-doc.org/core-2.7.0/Proc.html#method-i-curry Proc#curry
|
24
25
|
# @param arity [Integer]
|
25
26
|
# @return [Proc]
|
26
27
|
def curry(arity = nil)
|
@@ -31,19 +32,34 @@ module Invokable
|
|
31
32
|
#
|
32
33
|
# @return [Proc]
|
33
34
|
def memoize
|
34
|
-
|
35
|
+
lambda do |*args|
|
35
36
|
@memo ||= {}
|
36
37
|
@memo[args.hash] ||= call(*args)
|
37
38
|
end
|
38
39
|
end
|
39
40
|
|
40
|
-
# Return the arity (i.e. the number of arguments) of the
|
41
|
+
# Return the arity (i.e. the number of arguments) of the "call" method.
|
41
42
|
#
|
42
43
|
# @version 0.5.0
|
43
|
-
#
|
44
|
+
#
|
44
45
|
# @return [Integer]
|
45
46
|
def arity
|
46
47
|
method(:call).arity
|
47
48
|
end
|
49
|
+
|
50
|
+
# For Proc compatibility, forwards it's arguments to "call".
|
51
|
+
#
|
52
|
+
# @version 0.7.0
|
53
|
+
def [](*args)
|
54
|
+
call(*args)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Call invokable with one argument, allows invocables to be used in case statements
|
58
|
+
# and Enumerable#grep.
|
59
|
+
#
|
60
|
+
# @version 0.7.0
|
61
|
+
def ===(obj)
|
62
|
+
call(obj)
|
63
|
+
end
|
48
64
|
end
|
49
65
|
end
|
data/lib/invokable/data.rb
CHANGED
data/lib/invokable/hash.rb
CHANGED
@@ -1,12 +1,16 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
# Extend core Hash object by aliasing it's `dig` method as `call`,
|
4
|
-
# and including the `Invokable` module.
|
1
|
+
# Extend core Hash object by adding {to_proc} so hashes can be treated as functions of their keys.
|
5
2
|
#
|
6
|
-
# @
|
3
|
+
# @note {to_proc} was added to Hash in Ruby 2.7 so this patch is only applied for older versions of Ruby.
|
7
4
|
class Hash
|
8
5
|
if RUBY_VERSION.split('.').take(2).join('.').to_f < 2.7
|
9
|
-
|
10
|
-
|
6
|
+
# Return a proc that takes a key and returns the value associated with it or nil if the key
|
7
|
+
# is not present in the hash.
|
8
|
+
#
|
9
|
+
# @return [Proc]
|
10
|
+
def to_proc
|
11
|
+
lambda do |key|
|
12
|
+
fetch(key) { nil }
|
13
|
+
end
|
14
|
+
end
|
11
15
|
end
|
12
16
|
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module Invokable
|
2
|
+
# A collection of helper methods for working with invokables where they accept and invokable
|
3
|
+
# as an argument they will "coerce" the value (see {coerce}). This enables any method that
|
4
|
+
# implements `call` or `to_proc` to be treated as an invokable.
|
5
|
+
#
|
6
|
+
# @version 0.7.0
|
7
|
+
module Helpers
|
8
|
+
# Return a proc that returns the value that is passed to it.
|
9
|
+
#
|
10
|
+
# @version 0.7.0
|
11
|
+
#
|
12
|
+
# @return [Proc]
|
13
|
+
def identity
|
14
|
+
lambda do |x|
|
15
|
+
x
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Return a proc that will always return the value given it.
|
20
|
+
#
|
21
|
+
# @version 0.7.0
|
22
|
+
#
|
23
|
+
# @return [Proc]
|
24
|
+
def always(x)
|
25
|
+
lambda do
|
26
|
+
x
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# If the invokable passed responds to :call it will be returned. If it responds to to_proc to_proc
|
31
|
+
# is called and the resulting proc is returned. Otherwise a TypeError will be raised.
|
32
|
+
#
|
33
|
+
# @version 0.7.0
|
34
|
+
def coerce(invokable)
|
35
|
+
return invokable if invokable.respond_to?(:call)
|
36
|
+
return invokable.to_proc if invokable.respond_to?(:to_proc)
|
37
|
+
|
38
|
+
raise TypeError, "#{invokable.inspect} is not a valid invokable"
|
39
|
+
end
|
40
|
+
|
41
|
+
# Return a proc that passes it's arguments to the given invokables and returns an array of results.
|
42
|
+
# The invokables passed will be coerced before they are called (see {coerce}).
|
43
|
+
#
|
44
|
+
# @version 0.7.0
|
45
|
+
#
|
46
|
+
# @example
|
47
|
+
# juxtapose(:first, :count).call('A'..'Z') # => ["A", 26]
|
48
|
+
#
|
49
|
+
# @return [Proc]
|
50
|
+
def juxtapose(*invokables)
|
51
|
+
lambda do |*args|
|
52
|
+
invokables.map do |invokable|
|
53
|
+
coerce(invokable).call(*args)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
alias juxt juxtapose
|
58
|
+
|
59
|
+
# A relative of {juxtapose}--return a proc that takes a collection and calls the invokables
|
60
|
+
# on their corresponding values (in sequence) in the collection. The invokables passed
|
61
|
+
# will be coerced before they are called (see {coerce}).
|
62
|
+
#
|
63
|
+
# @version 0.7.0
|
64
|
+
#
|
65
|
+
# @example
|
66
|
+
# knit(:upcase, :downcase).call(['FoO', 'BaR']) # => ["FOO", "bar"]
|
67
|
+
#
|
68
|
+
# @return [Proc]
|
69
|
+
def knit(*invokables)
|
70
|
+
lambda do |enumerable|
|
71
|
+
results = []
|
72
|
+
enumerable.each_with_index do |x, i|
|
73
|
+
return results if invokables[i].nil?
|
74
|
+
|
75
|
+
results << coerce(invokables[i]).call(x)
|
76
|
+
end
|
77
|
+
results
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Return a proc that is a composition of the given invokables from right-to-left. The invokables
|
82
|
+
# passed will be coerced before they are called (see {coerce}).
|
83
|
+
#
|
84
|
+
# @version 0.7.0
|
85
|
+
#
|
86
|
+
# @example
|
87
|
+
# compose(:to_s, :upcase).call(:this_is_a_test) # => "THIS_IS_A_TEST"
|
88
|
+
#
|
89
|
+
# @return [Proc]
|
90
|
+
def compose(*invokables)
|
91
|
+
return identity if invokables.empty?
|
92
|
+
return coerce(invokables[0]) if invokables.count == 1
|
93
|
+
|
94
|
+
if invokables.count == 2
|
95
|
+
f, g = invokables
|
96
|
+
return lambda do |*args|
|
97
|
+
coerce(f).call(coerce(g).call(*args))
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
invokables.reduce do |a, b|
|
102
|
+
compose(a, b)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Return a proc that guards it's arguments from nil by replacing nil values with the alternative
|
107
|
+
# value that corresponds to it's place in the argument list. The invokable passed will be coerced
|
108
|
+
# before they are called (see {coerce}).
|
109
|
+
#
|
110
|
+
# @version 0.7.0
|
111
|
+
#
|
112
|
+
# @example
|
113
|
+
# count = guarded(:count, [])
|
114
|
+
# count.call(nil) # => 0
|
115
|
+
# count.call([1]) # => 1
|
116
|
+
#
|
117
|
+
# @return [Proc]
|
118
|
+
def guarded(invokable, *alternatives)
|
119
|
+
lambda do |*args|
|
120
|
+
new_args = args.each_with_index.map do |x, i|
|
121
|
+
x.nil? ? alternatives[i] : x
|
122
|
+
end
|
123
|
+
coerce(invokable).call(*new_args)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
alias fnil guarded
|
127
|
+
alias nil_safe guarded
|
128
|
+
|
129
|
+
# Given an invokable and and a fewer number of arguments that the invokable takes return
|
130
|
+
# a proc that will accept the rest of the arguments (i.e. a partialy applied function).
|
131
|
+
#
|
132
|
+
# @version 0.7.0
|
133
|
+
#
|
134
|
+
# @return [Proc]
|
135
|
+
def partial(invokable, *args)
|
136
|
+
lambda do |*other_args|
|
137
|
+
coerce(invokable).call(*(args + other_args))
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
data/lib/invokable/set.rb
CHANGED
@@ -1,11 +1,15 @@
|
|
1
1
|
require 'set'
|
2
|
-
require_relative 'core'
|
3
2
|
|
4
|
-
# Extend stdlib Set object by
|
5
|
-
#
|
6
|
-
#
|
7
|
-
# @see https://ruby-doc.org/stdlib-2.7.0/libdoc/set/rdoc/Set.html#method-i-include-3F Set#include?
|
3
|
+
# Extend stdlib Set object by adding {to_proc} which will allow a set to be treated as a function of
|
4
|
+
# the membership of it's elements.
|
8
5
|
class Set
|
9
|
-
|
10
|
-
|
6
|
+
# Return a proc that takes an value and return true if the value is an element of the set, but returns
|
7
|
+
# false otherwise.
|
8
|
+
#
|
9
|
+
# @return [Proc]
|
10
|
+
def to_proc
|
11
|
+
lambda do |element|
|
12
|
+
include?(element)
|
13
|
+
end
|
14
|
+
end
|
11
15
|
end
|
data/lib/invokable/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: invokable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Delon Newman
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-11-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -69,16 +69,18 @@ files:
|
|
69
69
|
- LICENSE.txt
|
70
70
|
- README.md
|
71
71
|
- Rakefile
|
72
|
+
- _config.yml
|
72
73
|
- invokable.gemspec
|
74
|
+
- lib/core_ext.rb
|
73
75
|
- lib/invokable.rb
|
74
76
|
- lib/invokable/array.rb
|
77
|
+
- lib/invokable/closure.rb
|
75
78
|
- lib/invokable/command.rb
|
76
79
|
- lib/invokable/compose.rb
|
77
80
|
- lib/invokable/core.rb
|
78
81
|
- lib/invokable/data.rb
|
79
82
|
- lib/invokable/hash.rb
|
80
|
-
- lib/invokable/
|
81
|
-
- lib/invokable/proc.rb
|
83
|
+
- lib/invokable/helpers.rb
|
82
84
|
- lib/invokable/set.rb
|
83
85
|
- lib/invokable/version.rb
|
84
86
|
homepage: https://github.com/delonnewman/invokable
|
data/lib/invokable/method.rb
DELETED