invokable 0.5.1 → 0.7.2

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