invokable 0.5.2 → 1.0.0

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: b107ca53f8d89104fa38152d27141d24ae62dbec10e8e313b1428b5e711f61e9
4
- data.tar.gz: b17e3b4207c4262637a08975772063f8d99583c6ef472220e18598885c272860
3
+ metadata.gz: 18c3cf809df359e8e89d764f5111d97b33ce63802d5e4ee1c01be83eed583d5c
4
+ data.tar.gz: b431129c01229cdac6751df4e5d03a756f0852ca9aa6e78cae7e2ff7d7caf36d
5
5
  SHA512:
6
- metadata.gz: c036b555078552a8902b83ccb7800ce878c23844a3ac6c7586d510622a56ba9ef0e4da985773662d5a605566308bb1c835b28eb988b417cc5a42a1c92aa95464
7
- data.tar.gz: 900fab835907e7a8af2f8284629361ed0fbe1db81e6d05d8a2a98152fc223e4a44de352eb1eba538a9e53d406e95d97bcf4ad82f133a96e91c282626999175a7
6
+ metadata.gz: 56d344edf79caf64a9dec52fba375b2455c889e097dbf5be2fdf074c2ac4ee0eaf3f882e4bf55f638cb47b3de1cc00b57e56cf47910731dea825be8042d1c074
7
+ data.tar.gz: 465820b6b31e9a99046098fd51ccc6eccc341e9aac0ef2595faca93abde00b6c9de5452fb54b5016b351eb73455c58ecdae8ed84dbb4505c5bb25345fa5e0046
@@ -1,20 +1,35 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
1
8
  name: Ruby
2
9
 
3
- on: [push]
10
+ on:
11
+ push:
12
+ branches: [ master ]
13
+ pull_request:
14
+ branches: [ master ]
4
15
 
5
16
  jobs:
6
- build:
17
+ test:
7
18
 
8
19
  runs-on: ubuntu-latest
20
+ strategy:
21
+ matrix:
22
+ ruby-version: ['2.4', '2.5', '2.6', '2.7', '3.0']
9
23
 
10
24
  steps:
11
25
  - uses: actions/checkout@v2
12
- - name: Set up Ruby 2.6
13
- uses: actions/setup-ruby@v1
26
+ - name: Set up Ruby
27
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
28
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
29
+ # uses: ruby/setup-ruby@v1
30
+ uses: ruby/setup-ruby@473e4d8fe5dd94ee328fdfca9f8c9c7afc9dae5e
14
31
  with:
15
- ruby-version: 2.6.x
16
- - name: Build and test with Rake
17
- run: |
18
- gem install bundler
19
- bundle install --jobs 4 --retry 3
20
- bundle exec rake
32
+ ruby-version: ${{ matrix.ruby-version }}
33
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
34
+ - name: Run tests
35
+ run: bundle exec rake
data/.gitignore CHANGED
@@ -10,3 +10,4 @@
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
12
  tags
13
+ .DS_Store
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.4
4
+ - 2.5
5
+ - 2.6
6
+ - 2.7
7
+ - 3.0
8
+ - jruby
@@ -1,11 +1,67 @@
1
1
  # Change Log
2
2
 
3
- ## 0.4.2
3
+ ## 1.0.0
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
+ - More robust testing
6
+ - Better error handling
7
+ - Keyword argument support
8
+
9
+ ## 0.7.2
10
+
11
+ Variable arity invoker methods are handled properly. Variable arity initializers will result in a more meaningful exception being raised.
12
+
13
+ ## 0.7.1
14
+
15
+ ### Invokable::call
16
+
17
+ When all the arguments of the initializer are passed and the instance method is zero arity the instance method will be called.
18
+
19
+ ```ruby
20
+ class Greeter
21
+ def initialize(name)
22
+ @name = name
23
+ end
24
+
25
+ def call
26
+ "Hello #{name}"
27
+ end
28
+ end
29
+
30
+ Greeter.call("Jane") # => "Hello Jane"
31
+ Greeter.call("John") # => #<Greeter ...> (before 0.7.1)
32
+ ```
33
+
34
+ ## 0.7.0
35
+
36
+ - Added helper methods `juxtapose`, `knit`, `compose`, `identity`, `always`, `guarded`, `partial`, and `coerce`
37
+ that can be used as module functions on `Invokable` or in classes that mixin the `Invokable` module.
38
+
39
+ - For classes whose initialize method takes no arguments, when the class method `call` is called it will
40
+ initialize the class and call it's `call` method.
41
+
42
+ - `[]` and `===` are added to classed that mixin `Invokable` for better `Proc` compatibility.
43
+
44
+ - `Array`, `Hash` and `Set` patches no longer include the invokable methods, they simply add `to_proc`.
45
+
46
+ - When `invokable/data` is required the array patch is also loaded.
47
+
48
+ - All the methods that take an invokable will "coerce" the invokable by simply returning it if it implements `call`
49
+ or coercing it into a proc if it implements `to_proc`.
50
+
51
+ ## 0.6.0
52
+
53
+ - `Invokable::Closure` deprecated comparable behavior has been added to `Invokable` itself.
54
+
55
+ ## 0.5.2
56
+
57
+ - `Invokable::Command` deprecated in favor of `Invokable::Closure`.
8
58
 
9
59
  ## 0.5.0
10
60
 
11
61
  - Added `Invokable::Command` and `Invokable::Core#arity`
62
+
63
+ ## 0.4.2
64
+
65
+ - `invokable/array` is no longer loaded with `invokable/data`.
66
+ This created a bit of havok in a few places. Including breaking
67
+ puma bootup in Rails 5.2.4.1.
data/Gemfile CHANGED
@@ -1,15 +1,7 @@
1
1
  source "https://rubygems.org"
2
2
 
3
- git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
-
5
- # Specify your gem's dependencies in data-functions.gemspec
6
3
  gemspec
7
4
 
8
5
  group :development do
9
- gem 'pry'
10
6
  gem 'yard'
11
7
  end
12
-
13
- group :test do
14
- gem 'gen-test'
15
- end
@@ -1,53 +1,38 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- invokable (0.5.2)
4
+ invokable (1.0.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
- coderay (1.1.2)
10
- concurrent-ruby (1.1.5)
11
- diff-lcs (1.3)
12
- faker (1.9.6)
13
- i18n (>= 0.7)
14
- gen-test (0.1.1)
15
- faker (~> 1.9.6)
16
- regexp-examples (~> 1.5.0)
17
- i18n (1.8.2)
18
- concurrent-ruby (~> 1.0)
19
- method_source (0.9.2)
20
- pry (0.12.2)
21
- coderay (~> 1.1.0)
22
- method_source (~> 0.9.0)
9
+ diff-lcs (1.4.4)
23
10
  rake (13.0.1)
24
- regexp-examples (1.5.1)
25
- rspec (3.9.0)
26
- rspec-core (~> 3.9.0)
27
- rspec-expectations (~> 3.9.0)
28
- rspec-mocks (~> 3.9.0)
29
- rspec-core (3.9.1)
30
- rspec-support (~> 3.9.1)
31
- rspec-expectations (3.9.0)
11
+ rspec (3.10.0)
12
+ rspec-core (~> 3.10.0)
13
+ rspec-expectations (~> 3.10.0)
14
+ rspec-mocks (~> 3.10.0)
15
+ rspec-core (3.10.0)
16
+ rspec-support (~> 3.10.0)
17
+ rspec-expectations (3.10.0)
32
18
  diff-lcs (>= 1.2.0, < 2.0)
33
- rspec-support (~> 3.9.0)
34
- rspec-mocks (3.9.1)
19
+ rspec-support (~> 3.10.0)
20
+ rspec-mocks (3.10.0)
35
21
  diff-lcs (>= 1.2.0, < 2.0)
36
- rspec-support (~> 3.9.0)
37
- rspec-support (3.9.2)
38
- yard (0.9.24)
22
+ rspec-support (~> 3.10.0)
23
+ rspec-support (3.10.0)
24
+ yard (0.9.25)
39
25
 
40
26
  PLATFORMS
27
+ java
41
28
  ruby
42
29
 
43
30
  DEPENDENCIES
44
- bundler (~> 1.17)
45
- gen-test
31
+ bundler (~> 2.1)
46
32
  invokable!
47
- pry
48
33
  rake (~> 13.0)
49
34
  rspec (~> 3.0)
50
35
  yard
51
36
 
52
37
  BUNDLED WITH
53
- 1.17.3
38
+ 2.2.6
data/README.md CHANGED
@@ -3,83 +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/closure'
96
+ require 'invokable/hash'
53
97
 
54
- class TwitterPoster
55
- include Invokable::Closure
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 :model
102
+ Arrays can be treated as functions of their indexes
58
103
 
59
- def call(user)
60
- # do the dirt
61
- ...
62
- TwitterStatus.new(user, data)
63
- end
64
- end
104
+ ```ruby
105
+ require 'invokable'
106
+ require 'invokable/array'
65
107
 
66
- TwitterPoster.call(Model.find(1)) # => #<TwitterPoster ...>
67
- 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
+ ```
68
111
 
69
- # both the class and it's instances can be used any where Procs are.
112
+ Sets can be treated as predicates
70
113
 
71
- Model.where(created_at: Date.today).map(&:TwitterPoster) # => [#<TwitterPoster ...>, ...]
114
+ ```ruby
115
+ require 'invokable'
116
+ require 'invokable/set'
117
+
118
+ favorite_numbers = Set[3, Math::PI]
119
+ [1, 2, 3, 4].select(&favorite_numbers) # => [3]
72
120
  ```
73
121
 
74
- Use as much or a little as you need:
122
+ Use as much or a little as you need
75
123
 
76
124
  ```ruby
77
125
  require 'invokable' # loads Invokable module
78
- require 'invokable/closure' # loads Invokable::Closure module
126
+ require 'invokable/helpers' # loads Invokable::Helpers module
79
127
  require 'invokable/hash' # loads hash patch
80
128
  require 'invokable/array' # loads array patch
81
129
  require 'invokable/set' # loads set patch
82
- require 'invokable/data' # loads hash and set patches
130
+ require 'invokable/data' # loads hash, set and array patches
83
131
  ```
84
132
 
85
133
  ## Why?
@@ -104,38 +152,9 @@ Or install it yourself as:
104
152
 
105
153
  > gem install invokable
106
154
 
107
- ## API
108
-
109
- ### `to_proc => Proc`
110
-
111
- ```ruby
112
- hash = { a: 1, b, 2 }
113
- hash.class # => Hash
114
- hash.to_proc.class # => Proc
115
- [:a, :b].map(&hash) # => [1, 2]
116
- ```
117
-
118
- Convert an object into a proc. When the `Invokable` module is included in a class it will do this by
119
- returning a proc that passes it's arguments to the object's `call` method. When `invokable/data` is
120
- loaded `Hash#call` is mapped to `Hash#dig`, `Array#call` is mapped to `Array#at`, and `Set#call`
121
- is mapped to `Set#include?`.
122
-
123
- ### `curry(arity = nil) => Proc`
124
-
125
- Returns a curried proc. If the `arity` is given, it determines the number of arguments.
126
- (see [Proc#curry](https://ruby-doc.org/core-2.7.0/Proc.html#method-i-curry)).
127
-
128
- ### `memoize => Proc`
129
-
130
- Returns a memoized proc, that is, a proc that caches it's return values by it's arguments.
131
-
132
- ### `<<(invokable) => Proc`
133
-
134
- Returns a proc that is a composition of this invokable and the given invokable.
135
-
136
- ### `>>(invokable) => Proc`
155
+ ## API Documentation
137
156
 
138
- 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)
139
158
 
140
159
  ## See Also
141
160
 
@@ -143,9 +162,9 @@ Returns a proc that is a composition of this invokable and the given invokable.
143
162
  - [Clojure](https://clojure.org)
144
163
  - [Arc](http://www.arclanguage.org)
145
164
 
146
- ## TODO
165
+ ## Support
147
166
 
148
- - benchmark Invokable#to_proc
167
+ Tested using MRI 2.4, 2.5, 2.6, 2.7, 3.0 and JRuby
149
168
 
150
169
  ## License
151
170
 
@@ -0,0 +1 @@
1
+ #theme: jekyll-theme-minimal
@@ -36,7 +36,7 @@ Gem::Specification.new do |spec|
36
36
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
37
37
  spec.require_paths = ["lib"]
38
38
 
39
- spec.add_development_dependency "bundler", "~> 1.17"
39
+ spec.add_development_dependency "bundler", "~> 2.1"
40
40
  spec.add_development_dependency "rake", "~> 13.0"
41
41
  spec.add_development_dependency "rspec", "~> 3.0"
42
42
  end
@@ -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,103 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'invokable/version'
2
4
  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
5
+ require 'invokable/helpers'
6
+ require 'core_ext'
10
7
 
8
+ # A module that attempts to generalize the notion of a first class function or Procs as they are often called
9
+ # in Ruby. It enables any class and it's objects to be treated as first-class functions. It also provides helpers
10
+ # includes helpers that can be used for performing higher-order operations on and object that can be treated
11
+ # as a function.
12
+ #
13
+ # @example
14
+ # class TwitterPoster
15
+ # include Invokable
16
+ #
17
+ # def initialize(model)
18
+ # @model = model
19
+ # end
20
+ #
21
+ # def call(user)
22
+ # # do the dirt
23
+ # ...
24
+ # TwitterStatus.new(user, data)
25
+ # end
26
+ # end
27
+ #
28
+ # TwitterPoster.call(Model.find(1)) # => #<TwitterPoster ...>
29
+ # TwitterPoster.call(Model.find(1), current_user) # => #<TwitterStatus ...>
11
30
  module Invokable
31
+ extend Invokable::Helpers
32
+
12
33
  def self.included(base)
13
- base.include(Invokable::Core)
14
- base.include(Invokable::Compose)
34
+ INCLUDED_MODULES.each do |mod|
35
+ base.include(mod)
36
+ base.extend(mod)
37
+ end
38
+ base.extend(ClassMethods)
39
+ end
40
+
41
+ private
42
+
43
+ INCLUDED_MODULES = [Core, Helpers].freeze
44
+ private_constant :INCLUDED_MODULES
45
+
46
+ # The methods that are mixed into any class at the class level that includes {Invokable}.
47
+ #
48
+ # @note The module should not be used directly.
49
+ module ClassMethods
50
+ # Return the "total" arity of the class (i.e. the arity of the initializer and the arity of the call method)
51
+ #
52
+ # @version 0.6.0
53
+ #
54
+ # @return [Integer]
55
+ def arity
56
+ return initializer_arity + invoker_arity if invoker_arity >= 0
57
+
58
+ (initializer_arity + invoker_arity.abs) * -1
59
+ end
60
+
61
+ # Handle automatic currying--will accept either the initializer arity or the total arity of the class. If
62
+ # the initializer arity is used return a class instance. If the total arity is used instantiate the class
63
+ # and return the results of the `call` method.
64
+ #
65
+ # @version 0.6.0
66
+ #
67
+ # @see arity
68
+ def call(*args, **kwargs)
69
+ initializer = initializer_arity
70
+ raise ArgumentError, "variable length initializer methods are not supported by Invokable try using `new' or `curry' instead" if initializer < 0
71
+ raise ArgumentError, "block arguments are not supported by Invokable" if block_given?
72
+
73
+ return new.call if arity.zero?
74
+
75
+ argc = kwargs.empty? ? args.length : args.length + 1
76
+ invoker = invoker_arity
77
+ return new(*args).call if argc == initializer && invoker.zero? && kwargs.empty?
78
+ return new(*args, **kwargs).call if argc == initializer && invoker.zero?
79
+ return new(*args).call if argc == initializer && invoker == -1 && kwargs.empty?
80
+ return new(*args).call(**kwargs) if argc == initializer && invoker == -1
81
+ return new(*args) if argc == initializer && kwargs.empty?
82
+ return new(*args, **kwargs) if argc == initializer
83
+
84
+ if argc == arity || invoker_arity < 0 && (args.length - initializer) >= (invoker.abs - 1)
85
+ init_args = args.slice(0, initializer)
86
+ call_args = args.slice(initializer, args.length)
87
+ return new(*init_args).call(*call_args) if kwargs.empty?
88
+ return new(*init_args).call(*call_args, **kwargs)
89
+ else
90
+ raise ArgumentError, "wrong number of arguments (given #{args.length}, expected #{arity})" if initializer == arity
91
+ raise ArgumentError, "wrong number of arguments (given #{args.length}, expected #{initializer} or #{arity})"
92
+ end
93
+ end
94
+
95
+ def initializer_arity
96
+ instance_method(:initialize).arity
97
+ end
98
+
99
+ def invoker_arity
100
+ instance_method(:call).arity
101
+ end
15
102
  end
16
103
  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
@@ -1,9 +1,9 @@
1
1
  module Invokable
2
2
  # Treat classes as curried functions
3
3
  #
4
- # @see https://ruby-doc.org/core-2.7.0/Proc.html#method-i-curry Proc#curry
5
- #
6
4
  # @version 0.5.2
5
+ #
6
+ # @deprecated These features are included in the module {Invokable} by default now.
7
7
  module Closure
8
8
  def self.included(klass)
9
9
  klass.include(Invokable)
@@ -31,9 +31,7 @@ module Invokable
31
31
  #
32
32
  # @return [Integer]
33
33
  def initializer_arity
34
- return @initializer_arity if @initializer_arity
35
-
36
- @initializer ? @initializer.arity : 0
34
+ instance_method(:initialize).arity
37
35
  end
38
36
 
39
37
  # Handle automatic currying--will accept either the initializer arity or the total arity of the class. If
@@ -3,9 +3,9 @@ require_relative 'closure'
3
3
  module Invokable
4
4
  # Treat "Command Objects" as curried functions
5
5
  #
6
- # @see https://ruby-doc.org/core-2.7.0/Proc.html#method-i-curry Proc#curry
7
- #
8
6
  # @version 0.5.0
7
+ #
8
+ # @deprecated These features are included in the {Invokable} module by default now.
9
9
  module Command
10
10
  def self.included(klass)
11
11
  klass.include(Invokable)
@@ -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.2"
2
+ VERSION = "1.0.0"
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.2
4
+ version: 1.0.0
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-28 00:00:00.000000000 Z
11
+ date: 2021-01-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.17'
19
+ version: '2.1'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.17'
26
+ version: '2.1'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -63,13 +63,16 @@ files:
63
63
  - ".github/workflows/ruby.yml"
64
64
  - ".gitignore"
65
65
  - ".rspec"
66
+ - ".travis.yml"
66
67
  - CHANGELOG.md
67
68
  - Gemfile
68
69
  - Gemfile.lock
69
70
  - LICENSE.txt
70
71
  - README.md
71
72
  - Rakefile
73
+ - _config.yml
72
74
  - invokable.gemspec
75
+ - lib/core_ext.rb
73
76
  - lib/invokable.rb
74
77
  - lib/invokable/array.rb
75
78
  - lib/invokable/closure.rb
@@ -78,8 +81,7 @@ files:
78
81
  - lib/invokable/core.rb
79
82
  - lib/invokable/data.rb
80
83
  - lib/invokable/hash.rb
81
- - lib/invokable/method.rb
82
- - lib/invokable/proc.rb
84
+ - lib/invokable/helpers.rb
83
85
  - lib/invokable/set.rb
84
86
  - lib/invokable/version.rb
85
87
  homepage: https://github.com/delonnewman/invokable
@@ -106,7 +108,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
106
108
  - !ruby/object:Gem::Version
107
109
  version: '0'
108
110
  requirements: []
109
- rubygems_version: 3.0.6
111
+ rubygems_version: 3.2.3
110
112
  signing_key:
111
113
  specification_version: 4
112
114
  summary: Objects are functions! Treat any Object, Hashes, Arrays and Sets as Procs
@@ -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