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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a5eab3dc64de36ef9f40927b0d4fa4d68fb0cc1cf867a2f94effa90a0872027
4
- data.tar.gz: 1fdc343357e9931e7dc5d62e71712159178cd44236ce6d50f7d37a88574d1403
3
+ metadata.gz: ff6115c057fbcfffd52d6037a190e9a36b23018f1c2b0745b2b9c7bb333cb86c
4
+ data.tar.gz: 4ebd7bdb25bdfbde7a762d333b9215f8e13909091fab11c1f048f52447e19b3c
5
5
  SHA512:
6
- metadata.gz: '0760094c175a2e23824bd07ef6830528b09d5ad3f331d1c07dcf35ab2a635d08c9ee3172ff3ef6c7edb0fc83d7d8da36dbd8e76417acfb700f940c6d962c79db'
7
- data.tar.gz: 278f13b9c4126e75704cffc87f472a2607acfeeabf10c3bd754f22ede54c5c1f2b073aaf399cb9f90f7085ce39bde425da04828648fc98386c1087b0a15e3a10
6
+ metadata.gz: 0d021ad9461294d8ee384d2302d25012b148e614c0deab1025d77da3a53355bbe62d326d9f29f15ed0685113523a577b13eef7a4d30ab81e323efaa59cecfb0a
7
+ data.tar.gz: 5469ed8b60c2d81cc89ad7f0a6debabf76b4f55e944f110365c2ed41db25379833f970acc288426ce61b123fffb6f3964cacc2d7ccc4689f9b7d3011b1654c02
data/.gitignore CHANGED
@@ -10,3 +10,4 @@
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
12
  tags
13
+ .DS_Store
@@ -1,11 +1,61 @@
1
1
  # Change Log
2
2
 
3
- ## 0.4.2
3
+ ## 0.7.2
4
4
 
5
- - `invokable/array` is no longer loaded with `invokable/data`.
6
- This created a bit of havok in a few places. Including breaking
7
- puma bootup in Rails 5.2.4.1.
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.
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- invokable (0.5.1)
4
+ invokable (0.7.2)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -3,85 +3,131 @@
3
3
 
4
4
  # Invokable
5
5
 
6
- Objects are functions! Treat any Object, Hashes, Arrays, and Sets as Procs (like Enumerable but for Procs)
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
- number_names = { 1 => "One", 2 => "Two", 3 => "Three" }
15
- [1, 2, 3, 4].map(&number_names) # => ["One", "Two", "Three", nil]
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
- require 'invokable'
20
- require 'invokable/array'
40
+ # app/queries/filter_records.rb
41
+ class FilterRecords
42
+ include Invokable
21
43
 
22
- alpha = ('a'..'z').to_a
23
- [1, 2, 3, 4].map(&alpha) # => ["b", "c", "d", "e"]
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
- require 'invokable'
28
- require 'invokable/set'
73
+ Invokable.juxtapose(:sum, -> (xs) { xs.reduce(:*) }, :min, :max).([3, 4, 6]) # => [13, 72, 3, 6]
29
74
 
30
- favorite_numbers = Set[3, Math::PI]
31
- [1, 2, 3, 4].select(&favorite_numbers) # => [3]
75
+ Invokable.knit(:upcase, :downcase).(['FoO', 'BaR']) # => ["FOO", "bar"]
32
76
  ```
33
77
 
34
- ```ruby
35
- # service objects
36
- require 'invokable'
78
+ They are also mixed into any class that includes the module
37
79
 
38
- class GetDataFromSomeService
80
+ ```ruby
81
+ class Transformer
39
82
  include Invokable
40
83
 
41
- def call(user)
42
- # do the dirt
84
+ def call(array)
85
+ array.map(&juxtapose(identity, compose(:to_s, :upcase))).to_h
43
86
  end
44
87
  end
45
88
 
46
- data_for_user = GetDataFromSomeService.new.memoize # 'memoize' makes a proc that caches results
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/command'
96
+ require 'invokable/hash'
53
97
 
54
- class TwitterPoster
55
- include Invokable::Command
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
- enclose do |model|
58
- @model = model
59
- end
102
+ Arrays can be treated as functions of their indexes
60
103
 
61
- def call(user)
62
- # do the dirt
63
- ...
64
- TwitterStatus.new(user, data)
65
- end
66
- end
104
+ ```ruby
105
+ require 'invokable'
106
+ require 'invokable/array'
67
107
 
68
- TwitterPoster.call(Model.find(1)) # => #<TwitterPoster ...>
69
- TwitterPoster.call(Model.find(1), current_user) # => #<TwitterStatus ...>
108
+ alpha = ('a'..'z').to_a
109
+ [1, 2, 3, 4].map(&alpha) # => ["b", "c", "d", "e"]
110
+ ```
70
111
 
71
- # both the class and it's instances can be used any where Procs are.
112
+ Sets can be treated as predicates
113
+
114
+ ```ruby
115
+ require 'invokable'
116
+ require 'invokable/set'
72
117
 
73
- Model.where(created_at: Date.today).map(&:TwitterPoster) # => [#<TwitterPoster ...>, ...]
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/command' # loads Invokable::Command module
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 set patches
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
- ### `to_proc => Proc`
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).
@@ -0,0 +1 @@
1
+ #theme: jekyll-theme-minimal
@@ -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
@@ -1,16 +1,89 @@
1
1
  require 'invokable/version'
2
2
  require 'invokable/core'
3
- require 'invokable/compose'
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
- base.include(Invokable::Core)
14
- base.include(Invokable::Compose)
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
@@ -1,10 +1,13 @@
1
- require_relative 'core'
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
- include Invokable::Core
9
- alias call []
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
@@ -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
@@ -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 `invokable`.
4
- # The returned proc takes a variable number of arguments, calls `invokable` with
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
- Proc.new do |*args|
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 `invokable`.
15
- # The returned proc takes a variable number of arguments, calls `invokable` with
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
- Proc.new do |*args|
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
@@ -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
- # If object responds to `call` convert into a Proc forwards it's arguments along to `call`.
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
- if respond_to?(:call)
9
- # TODO: Would method(:call) be more performant? We need benchmarks.
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 `arity` argument is given, it determines the number of arguments.
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
- Proc.new do |*args|
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 `call` method.
41
+ # Return the arity (i.e. the number of arguments) of the "call" method.
41
42
  #
42
43
  # @version 0.5.0
43
- # @see https://ruby-doc.org/core-2.7.1/Proc.html#method-i-arity Proc#arity
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
@@ -1,2 +1,3 @@
1
1
  require_relative 'hash'
2
2
  require_relative 'set'
3
+ require_relative 'array'
@@ -1,12 +1,16 @@
1
- require_relative 'core'
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
- # @see https://ruby-doc.org/core-2.7.0/Hash.html#method-i-dig Hash#dig
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
- include Invokable::Core
10
- alias call dig
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
@@ -1,11 +1,15 @@
1
1
  require 'set'
2
- require_relative 'core'
3
2
 
4
- # Extend stdlib Set object by aliasing it's `include?` method as `call`,
5
- # and including the `Invokable` module.
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
- include Invokable::Core
10
- alias call include?
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
@@ -1,3 +1,3 @@
1
1
  module Invokable
2
- VERSION = "0.5.1"
2
+ VERSION = "0.7.2"
3
3
  end
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.5.1
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-07-21 00:00:00.000000000 Z
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/method.rb
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
@@ -1,5 +0,0 @@
1
- require_relative 'compose'
2
-
3
- class Method
4
- include Invokable::Compose
5
- end
@@ -1,5 +0,0 @@
1
- require_relative 'compose'
2
-
3
- class Proc
4
- include Invokable::Compose
5
- end