invokable 0.5.2 → 1.0.0

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: 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