functional-ruby 0.7.7 → 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 +4 -4
- data/README.md +92 -152
- data/doc/memo.txt +192 -0
- data/doc/pattern_matching.txt +485 -0
- data/doc/protocol.txt +221 -0
- data/doc/record.txt +144 -0
- data/doc/thread_safety.txt +8 -0
- data/lib/functional.rb +48 -18
- data/lib/functional/abstract_struct.rb +161 -0
- data/lib/functional/delay.rb +117 -0
- data/lib/functional/either.rb +222 -0
- data/lib/functional/memo.rb +93 -0
- data/lib/functional/method_signature.rb +72 -0
- data/lib/functional/option.rb +209 -0
- data/lib/functional/pattern_matching.rb +117 -100
- data/lib/functional/protocol.rb +157 -0
- data/lib/functional/protocol_info.rb +193 -0
- data/lib/functional/record.rb +155 -0
- data/lib/functional/type_check.rb +112 -0
- data/lib/functional/union.rb +152 -0
- data/lib/functional/version.rb +3 -1
- data/spec/functional/abstract_struct_shared.rb +154 -0
- data/spec/functional/complex_pattern_matching_spec.rb +205 -0
- data/spec/functional/configuration_spec.rb +17 -0
- data/spec/functional/delay_spec.rb +147 -0
- data/spec/functional/either_spec.rb +237 -0
- data/spec/functional/memo_spec.rb +207 -0
- data/spec/functional/option_spec.rb +292 -0
- data/spec/functional/pattern_matching_spec.rb +279 -276
- data/spec/functional/protocol_info_spec.rb +444 -0
- data/spec/functional/protocol_spec.rb +274 -0
- data/spec/functional/record_spec.rb +175 -0
- data/spec/functional/type_check_spec.rb +103 -0
- data/spec/functional/union_spec.rb +110 -0
- data/spec/spec_helper.rb +6 -4
- metadata +55 -45
- data/lib/functional/behavior.rb +0 -138
- data/lib/functional/behaviour.rb +0 -2
- data/lib/functional/catalog.rb +0 -487
- data/lib/functional/collection.rb +0 -403
- data/lib/functional/inflect.rb +0 -127
- data/lib/functional/platform.rb +0 -120
- data/lib/functional/search.rb +0 -132
- data/lib/functional/sort.rb +0 -41
- data/lib/functional/utilities.rb +0 -189
- data/md/behavior.md +0 -188
- data/md/catalog.md +0 -32
- data/md/collection.md +0 -32
- data/md/inflect.md +0 -32
- data/md/pattern_matching.md +0 -512
- data/md/platform.md +0 -32
- data/md/search.md +0 -32
- data/md/sort.md +0 -32
- data/md/utilities.md +0 -55
- data/spec/functional/behavior_spec.rb +0 -528
- data/spec/functional/catalog_spec.rb +0 -1206
- data/spec/functional/collection_spec.rb +0 -752
- data/spec/functional/inflect_spec.rb +0 -85
- data/spec/functional/integration_spec.rb +0 -205
- data/spec/functional/platform_spec.rb +0 -501
- data/spec/functional/search_spec.rb +0 -187
- data/spec/functional/sort_spec.rb +0 -61
- data/spec/functional/utilities_spec.rb +0 -277
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fe9f8b8bb0900055e86688c7995156bb89b5c0af
|
4
|
+
data.tar.gz: 794627125c2647c8e813b873c69f9be62bf3f206
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b2e54bc22942b8e2be11ca19fe1c0bd393a1a868bff7f2717d5830efea7e048b53f574763352b778170fe6e46d76d7e42bd566200843595a97142a35572a7941
|
7
|
+
data.tar.gz: 21c78e069e28438cc55dcdd3f0a2dc0892a5f36236337cd42a28dc8597d04b33f9565a3a46b03ce8c405b3b7b80ade995e2cd4f24c01642335382eaaaf917fc6
|
data/README.md
CHANGED
@@ -1,70 +1,29 @@
|
|
1
|
-
# Functional Ruby
|
1
|
+
# Functional Ruby
|
2
|
+
[](http://badge.fury.io/rb/functional-ruby) [](https://travis-ci.org/jdantonio/functional-ruby?branch=master) [](https://coveralls.io/r/jdantonio/functional-ruby) [](https://codeclimate.com/github/jdantonio/functional-ruby) [](http://inch-ci.org/github/jdantonio/functional-ruby) [](https://gemnasium.com/jdantonio/functional-ruby)
|
2
3
|
|
3
|
-
A gem for adding
|
4
|
+
**A gem for adding functional programming tools to Ruby. Inspired by [Erlang](http://www.erlang.org/),
|
5
|
+
[Clojure](http://clojure.org/), and [Functional Java](http://functionaljava.org/).**
|
4
6
|
|
5
|
-
*NOTE:
|
6
|
-
|
7
|
-
|
8
|
-
The project is hosted on the following sites:
|
9
|
-
|
10
|
-
* [RubyGems project page](https://rubygems.org/gems/functional-ruby)
|
11
|
-
* [Source code on GitHub](https://github.com/jdantonio/functional-ruby)
|
12
|
-
* [YARD documentation on RubyDoc.info](http://rubydoc.info/github/jdantonio/functional-ruby/frames)
|
13
|
-
* [Continuous integration on Travis-CI](https://travis-ci.org/jdantonio/functional-ruby)
|
14
|
-
* [Dependency tracking on Gemnasium](https://gemnasium.com/jdantonio/functional-ruby)
|
15
|
-
* [Follow me on Twitter](https://twitter.com/jerrydantonio)
|
7
|
+
*NOTE: Version 1.0 is a complete rewrite. Previous versions lacked a unified
|
8
|
+
focus. Version 1.0 is a cohesive set of utilities inspired by other languages
|
9
|
+
but designed to work together in ways idiomatic to Ruby.*
|
16
10
|
|
17
11
|
## Introduction
|
18
12
|
|
19
13
|
Two things I love are [Ruby](http://www.ruby-lang.org/en/) and
|
20
14
|
[functional](https://en.wikipedia.org/wiki/Functional_programming)
|
21
15
|
[programming](http://c2.com/cgi/wiki?FunctionalProgramming).
|
22
|
-
Sadly, the former is generally not associated with the latter. Unfortunately,
|
23
|
-
too many people are blinded by their belief that Ruby is an object-oriented
|
24
|
-
language. I reject this assertion. Ruby is certainly object-based, since
|
25
|
-
everything is an object, but entire large-scale programs can be built without ever
|
26
|
-
defining a single class. But Ruby's features that support functional programming
|
27
|
-
don't stop there.
|
28
|
-
|
29
|
-
Ask ten different programmers to define the term "functional programming" and
|
30
|
-
you will likely get ten different answers. One characteristic that will certainly
|
31
|
-
be on all their lists is support for
|
32
|
-
[higher](http://en.wikipedia.org/wiki/Higher-order_function)
|
33
|
-
[order](http://learnyouahaskell.com/higher-order-functions)
|
34
|
-
[functions](http://learnyousomeerlang.com/higher-order-functions). Put simply, a
|
35
|
-
higher order function is any function that can take one or more functions as
|
36
|
-
parameters and/or return a function as a result. Many functional languages,
|
37
|
-
such as Erlang, Haskell, and Closure, support higher order functions. Higher order
|
38
|
-
functions are a remarkable tool that can completely change the was software is
|
39
|
-
designed. Most classicaly object-oriented languages do not support higher
|
40
|
-
order functions. Unfortunately, Ruby does not directly support higher order
|
41
|
-
functions. Thanksfully, Ruby *does* give us blocks, `proc`s, and `lambda`s.
|
42
|
-
Though not strictly higher order functions, in most cases they are functionally
|
43
|
-
equivalent.
|
44
|
-
|
45
16
|
If you combine Ruby's ability to create functions sans-classes with the power
|
46
|
-
of blocks
|
47
|
-
programming design paradigm.
|
48
|
-
|
49
|
-
most powerful languages in common use today.
|
50
|
-
|
51
|
-
This gem is my small and humble attempt to help Ruby reach its full potential as
|
52
|
-
a highly performant, functional programming language. Virtually every function in
|
53
|
-
this library takes a block parameter. Some allow a block plus one or more `proc`
|
54
|
-
arguments. Most operate *on* data structures rather than being buried *in* data
|
55
|
-
structures. Finally, several of the tools in this library are Ruby implementations
|
56
|
-
of some of my favorite features from other functional programming languages. Not
|
57
|
-
every function is pure, but functions with side effects are easy to spot because
|
58
|
-
they almost always have names that end in an exclamation point.
|
59
|
-
|
60
|
-
My hope is that this gem will help Ruby programmers explore Ruby as a functional
|
61
|
-
language and improve their code in ways our object oriented brethern never
|
62
|
-
dreamed possible.
|
17
|
+
of blocks, `proc`, and `lambda`, Ruby code can follow just about every modern functional
|
18
|
+
programming design paradigm. Add to this Ruby's vast metaprogramming capabilities
|
19
|
+
and Ruby is easily one of the most powerful languages in common use today.
|
63
20
|
|
64
21
|
### Goals
|
65
22
|
|
66
|
-
|
23
|
+
Our goal is to implement various functional programming patterns in Ruby. Specifically:
|
67
24
|
|
25
|
+
* Be an 'unopinionated' toolbox that provides useful utilities without debating which is better or why
|
26
|
+
* Remain free of external gem dependencies
|
68
27
|
* Stay true to the spirit of the languages providing inspiration
|
69
28
|
* But implement in a way that makes sense for Ruby
|
70
29
|
* Keep the semantics as idiomatic Ruby as possible
|
@@ -73,27 +32,26 @@ My goal is to implement various functional programming patterns in Ruby. Specifi
|
|
73
32
|
* Keep everything small
|
74
33
|
* Be as fast as reasonably possible
|
75
34
|
|
76
|
-
## Features
|
77
|
-
|
78
|
-
Several features from Erlang, Go, and Clojure have been implemented thus far:
|
79
|
-
|
80
|
-
* Interface specifications with Erlang-style [Behavior](https://github.com/jdantonio/functional-ruby/blob/master/md/behavior.md)
|
81
|
-
* A [Catalog](https://github.com/jdantonio/functional-ruby/blob/master/md/catalog.md) class for managing sets of key/value pairs in a manner similar to Erlang's [proplists](http://www.erlang.org/doc/man/proplists.html)
|
82
|
-
* A toolkit of [collection](https://github.com/jdantonio/functional-ruby/blob/master/md/collection.md) utilities for operating on list-like data structures
|
83
|
-
* A set of string [inflections](https://github.com/jdantonio/functional-ruby/blob/master/md/inflect.md) borrowed from [Active Support](http://guides.rubyonrails.org/active_support_core_extensions.html#inflections)
|
84
|
-
* Function overloading with Erlang-style [Pattern Matching](https://github.com/jdantonio/functional-ruby/blob/master/md/pattern_matching.md)
|
85
|
-
* Tools for introspecting the runtime [Platform](https://github.com/jdantonio/functional-ruby/blob/master/md/platform.md) for information about the operating system and Ruby version
|
86
|
-
* [Search](https://github.com/jdantonio/functional-ruby/blob/master/md/search.md) and [sort](https://github.com/jdantonio/functional-ruby/blob/master/md/sort.md) algorithms like you remember from your algorithms class, but with a functional twist
|
87
|
-
* Several useful functional [Utilities](https://github.com/jdantonio/functional-ruby/blob/master/md/utilities.md)
|
35
|
+
## Features
|
88
36
|
|
89
|
-
|
37
|
+
Complete API documentation can be found at [Rubydoc.info](http://rubydoc.info/github/jdantonio/functional-ruby/master/frames).
|
90
38
|
|
91
|
-
[
|
39
|
+
* Protocol specifications inspired by Clojure [protocol](http://clojure.org/protocols),
|
40
|
+
Erlang [behavior](http://www.erlang.org/doc/design_principles/des_princ.html#id60128),
|
41
|
+
and Objective-C [protocol](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithProtocols/WorkingwithProtocols.html)
|
42
|
+
* Function overloading with Erlang-style [function](http://erlang.org/doc/reference_manual/functions.html)
|
43
|
+
[pattern matching](http://erlang.org/doc/reference_manual/patterns.html)
|
44
|
+
* Simple, immutable data structures, such as *record* and *union*, inspired by
|
45
|
+
[Clojure](http://clojure.org/datatypes), [Erlang](http://www.erlang.org/doc/reference_manual/records.html),
|
46
|
+
and [others](http://en.wikipedia.org/wiki/Union_type)
|
47
|
+
* `Either` and `Option` classes based on [Functional Java](http://functionaljava.org/) and [Haskell](https://hackage.haskell.org/package/base-4.2.0.1/docs/Data-Either.html)
|
48
|
+
* [Memoization](http://en.wikipedia.org/wiki/Memoization) of class methods based on Clojure [memoize](http://clojuredocs.org/clojure_core/clojure.core/memoize)
|
49
|
+
* Lazy execution with a `Delay` class based on Clojure [delay](http://clojuredocs.org/clojure_core/clojure.core/delay)
|
92
50
|
|
93
|
-
### Supported Ruby
|
51
|
+
### Supported Ruby Versions
|
94
52
|
|
95
|
-
MRI
|
96
|
-
It should be fully compatible with any
|
53
|
+
MRI 2.0 and higher, JRuby (1.9 mode), and Rubinius 2.x. This library is pure Ruby and has no gem dependencies.
|
54
|
+
It should be fully compatible with any interpreter that is compliant with Ruby 2.0 or newer.
|
97
55
|
|
98
56
|
### Install
|
99
57
|
|
@@ -115,110 +73,92 @@ Once you've installed the gem you must `require` it in your project:
|
|
115
73
|
require 'functional'
|
116
74
|
```
|
117
75
|
|
118
|
-
|
76
|
+
## Examples
|
119
77
|
|
120
|
-
|
121
|
-
few examples to whet your appetite.
|
78
|
+
Specifying a protocol:
|
122
79
|
|
123
|
-
|
80
|
+
```ruby
|
81
|
+
Functional::SpecifyProtocol(:Name) do
|
82
|
+
attr_accessor :first
|
83
|
+
attr_accessor :middle
|
84
|
+
attr_accessor :last
|
85
|
+
attr_accessor :suffix
|
86
|
+
end
|
87
|
+
```
|
124
88
|
|
125
|
-
|
89
|
+
Pattern matching using protocols, type checking, and other options:
|
126
90
|
|
127
91
|
```ruby
|
128
|
-
require 'functional/pattern_matching'
|
129
|
-
|
130
92
|
class Foo
|
131
|
-
include PatternMatching
|
93
|
+
include Functional::PatternMatching
|
94
|
+
include Functional::Protocol
|
95
|
+
include Functional::TypeChecking
|
132
96
|
|
133
|
-
|
134
|
-
|
97
|
+
def greet
|
98
|
+
return 'Hello, World!'
|
99
|
+
end
|
100
|
+
|
101
|
+
defn(:greet, _) do |name|
|
102
|
+
"Hello, #{name}!"
|
103
|
+
end
|
104
|
+
|
105
|
+
defn(:greet, _) { |name|
|
106
|
+
"Pleased to meet you, #{name.fulle_name}!"
|
107
|
+
}.when {|name| Type?(CustomerModel, ClientModel) }
|
108
|
+
|
109
|
+
defn(:greet, _) { |name|
|
110
|
+
"Hello, #{name.first} #{name.last}!"
|
111
|
+
}.when {|name| Satisfy?(:Name) }
|
112
|
+
|
113
|
+
defn(:greet, :male, _) { |name|
|
114
|
+
"Hello, Mr. #{name}!"
|
135
115
|
}
|
136
116
|
|
137
|
-
defn(:greet, :female) {
|
138
|
-
|
117
|
+
defn(:greet, :female, _) { |name|
|
118
|
+
"Hello, Ms. #{name}!"
|
139
119
|
}
|
140
|
-
end
|
141
120
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
```
|
121
|
+
defn(:greet, nil, _) { |name|
|
122
|
+
"Goodbye, #{name}!"
|
123
|
+
}
|
146
124
|
|
147
|
-
|
125
|
+
defn(:greet, _, _) { |_, name|
|
126
|
+
"Hello, #{name}!"
|
127
|
+
}
|
128
|
+
end
|
129
|
+
```
|
148
130
|
|
149
|
-
|
131
|
+
Memoization:
|
150
132
|
|
151
133
|
```ruby
|
152
|
-
|
153
|
-
|
154
|
-
behaviour_info(:gen_foo, foo: 0, self_bar: 1)
|
134
|
+
class Factors
|
135
|
+
include Functional::Memo
|
155
136
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
def foo
|
160
|
-
return 'foo/0'
|
137
|
+
def self.sum_of(number)
|
138
|
+
of(number).reduce(:+)
|
161
139
|
end
|
162
140
|
|
163
|
-
def self.
|
164
|
-
|
141
|
+
def self.of(number)
|
142
|
+
(1..number).select {|i| factor?(number, i)}
|
165
143
|
end
|
166
|
-
end
|
167
144
|
|
168
|
-
|
145
|
+
def self.factor?(number, potential)
|
146
|
+
number % potential == 0
|
147
|
+
end
|
169
148
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
'foo'.behaves_as? :gen_foo #=> false
|
149
|
+
memoize(:sum_of)
|
150
|
+
memoize(:of)
|
151
|
+
end
|
174
152
|
```
|
175
153
|
|
176
|
-
|
177
|
-
|
178
|
-
Documentation: [Utilities](https://github.com/jdantonio/functional-ruby/blob/master/md/utilities.md)
|
179
|
-
|
180
|
-
```ruby
|
181
|
-
Infinity #=> Infinity
|
182
|
-
NaN #=> NaN
|
183
|
-
|
184
|
-
repl? #=> true when called under irb, pry, bundle console, or rails console
|
185
|
-
|
186
|
-
safe(1, 2){|a, b| a + b} #=> 3
|
187
|
-
safe{ eval 'puts "Hello World!"' } #=> SecurityError: Insecure operation
|
154
|
+
## Contributing
|
188
155
|
|
189
|
-
|
156
|
+
1. Fork it
|
157
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
158
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
159
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
160
|
+
5. Create new Pull Request
|
190
161
|
|
191
|
-
|
192
|
-
delta({count: -1}, {count: 1}){|item| item[:count]} #=> 2
|
193
|
-
|
194
|
-
# And many more!
|
195
|
-
```
|
162
|
+
## License and Copyright
|
196
163
|
|
197
|
-
|
198
|
-
|
199
|
-
*Functional Ruby* is Copyright © 2013 [Jerry D'Antonio](https://twitter.com/jerrydantonio).
|
200
|
-
It is free software and may be redistributed under the terms specified in the LICENSE file.
|
201
|
-
|
202
|
-
## License
|
203
|
-
|
204
|
-
Released under the MIT license.
|
205
|
-
|
206
|
-
http://www.opensource.org/licenses/mit-license.php
|
207
|
-
|
208
|
-
> Permission is hereby granted, free of charge, to any person obtaining a copy
|
209
|
-
> of this software and associated documentation files (the "Software"), to deal
|
210
|
-
> in the Software without restriction, including without limitation the rights
|
211
|
-
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
212
|
-
> copies of the Software, and to permit persons to whom the Software is
|
213
|
-
> furnished to do so, subject to the following conditions:
|
214
|
-
>
|
215
|
-
> The above copyright notice and this permission notice shall be included in
|
216
|
-
> all copies or substantial portions of the Software.
|
217
|
-
>
|
218
|
-
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
219
|
-
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
220
|
-
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
221
|
-
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
222
|
-
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
223
|
-
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
224
|
-
> THE SOFTWARE.
|
164
|
+
*Functional Ruby* is free software released under the [MIT License](http://www.opensource.org/licenses/MIT).
|
data/doc/memo.txt
ADDED
@@ -0,0 +1,192 @@
|
|
1
|
+
# @!macro [new] memoize
|
2
|
+
#
|
3
|
+
# ## Rationale
|
4
|
+
#
|
5
|
+
# Many computational operations take a significant amount of time and/or use
|
6
|
+
# an inordinate amount of resources. If subsequent calls to that function with
|
7
|
+
# the same parameters are guaranteed to return the same result, caching the
|
8
|
+
# result can lead to significant performance improvements. The process of
|
9
|
+
# caching such calls is called
|
10
|
+
# [memoization](http://en.wikipedia.org/wiki/Memoization).
|
11
|
+
#
|
12
|
+
# ## Declaration
|
13
|
+
#
|
14
|
+
# Using memoization requires two simple steps: including the
|
15
|
+
# `Functional::Memo` module within a class or module and calling the `memoize`
|
16
|
+
# function to enable memoization on one or more methods.
|
17
|
+
#
|
18
|
+
# ```ruby
|
19
|
+
# Module EvenNumbers
|
20
|
+
# include Functional::Memoize
|
21
|
+
#
|
22
|
+
# self.first(n)
|
23
|
+
# (2..n).select{|i| i % 2 == 0 }
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# memoize :first
|
27
|
+
# end
|
28
|
+
# ```
|
29
|
+
#
|
30
|
+
# When a function is memoized an internal cache is created that maps arguments
|
31
|
+
# to return values. When the function is called the arguments are checked
|
32
|
+
# against the cache. If the args are found the method is not called and the
|
33
|
+
# cached result is returned instead.
|
34
|
+
#
|
35
|
+
# ## Ramifications
|
36
|
+
#
|
37
|
+
# Memoizing long-running methods can lead to significant performance
|
38
|
+
# advantages. But there is a trade-off. Memoization may greatly increase the
|
39
|
+
# memory footprint of the application. The memo cache itself takes memory. The
|
40
|
+
# more arg/result pairs stored in the cache, the more memory is consumed.
|
41
|
+
#
|
42
|
+
# ### Cache Size Options
|
43
|
+
#
|
44
|
+
# To help control the size of the cache, a limit can be placed on the number
|
45
|
+
# of items retained in the cache. The `:at_most` option, when given, indicates
|
46
|
+
# the maximum size of the cache. Once the maximum cache size is reached, calls
|
47
|
+
# to to the method with uncached args will still result in the method being
|
48
|
+
# called, but the results will not be cached.
|
49
|
+
#
|
50
|
+
# ```ruby
|
51
|
+
# Module EvenNumbers
|
52
|
+
# include Functional::Memoize
|
53
|
+
#
|
54
|
+
# self.first(n)
|
55
|
+
# (2..n).select{|i| i % 2 == 0 }
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# memoize :first, at_most: 1000
|
59
|
+
# end
|
60
|
+
# ```
|
61
|
+
#
|
62
|
+
# There is no way to predict in advance what the proper cache size is, or if
|
63
|
+
# it should be restricted at all. Only performance testing under realistic
|
64
|
+
# conditions or profiling of a running system can provide guidance.
|
65
|
+
#
|
66
|
+
# ## Restrictions
|
67
|
+
#
|
68
|
+
# Not all methods are good candidates for memoization.Only methods that are
|
69
|
+
# [idempotent](http://en.wikipedia.org/wiki/Idempotence), [referentially
|
70
|
+
# transparent](http://en.wikipedia.org/wiki/Referential_transparency_(computer_science)),
|
71
|
+
# and free of [side effects](http://en.wikipedia.org/wiki/Side_effect_(computer_science))
|
72
|
+
# can be effectively memoized. If a method creates side effects, such as
|
73
|
+
# writing to a log, only the first call to the method will create those side
|
74
|
+
# effects. Subsequent calls will return the cached value without calling the
|
75
|
+
# method.
|
76
|
+
#
|
77
|
+
# Similarly, methods which change internal state will only update the state on
|
78
|
+
# the initial call. Later calls will not result in state changes, they will
|
79
|
+
# only return the original result. Subsequently, instance methods cannot be
|
80
|
+
# memoized. Objects are, by definition, stateful. Method calls exist for the
|
81
|
+
# purpose of changing or using the internal state of the object. Such methods
|
82
|
+
# cannot be effectively memoized; it would require the internal state of the
|
83
|
+
# object to be cached and checked as well.
|
84
|
+
#
|
85
|
+
# Block parameters pose a similar problem. Block parameters are inherently
|
86
|
+
# stateful (they are closures which capture the enclosing context). And there
|
87
|
+
# is no way to check the state of the block along with the args to determine
|
88
|
+
# if the cached value should be used. Subsequently, and method call which
|
89
|
+
# includes a block will result in the cache being completely skipped. The base
|
90
|
+
# method will be called and the result will not be cached. This behavior will
|
91
|
+
# occur even when the given method was not programmed to accept a block
|
92
|
+
# parameter. Ruby will capture any block passed to any method and make it
|
93
|
+
# available to the method even when not documented as a formal parameter or
|
94
|
+
# used in the method. This has the interesting side effect of allowing the
|
95
|
+
# memo cache to be skipped on any method call, simply be passing a block
|
96
|
+
# parameter.
|
97
|
+
#
|
98
|
+
# ```ruby
|
99
|
+
# EvenNumbers.first(100) # causes the result to be cached
|
100
|
+
# EvenNumbers.first(100) # retrieves the previous result from the cache
|
101
|
+
# EvenNumbers.first(100){ nil } # skips the memo cache and calls the method again
|
102
|
+
# ```
|
103
|
+
#
|
104
|
+
# ### Complete Example
|
105
|
+
#
|
106
|
+
# The following example is borrowed from the book [Functional Thinking](http://shop.oreilly.com/product/0636920029687.do)
|
107
|
+
# by Neal Ford. In his book he shows an example of memoization in Groovy by
|
108
|
+
# summing factors of a given number. This is a great example because it
|
109
|
+
# exhibits all the criteria that make a method a good memoization candidate:
|
110
|
+
#
|
111
|
+
# * Idempotence
|
112
|
+
# * Referential transparency
|
113
|
+
# * Stateless
|
114
|
+
# * Free of side effect
|
115
|
+
# * Computationally expensive (for large numbers)
|
116
|
+
#
|
117
|
+
# The following code implements Ford's algorithms in Ruby, then memoizes two
|
118
|
+
# key methods. The Ruby code:
|
119
|
+
#
|
120
|
+
# ```ruby
|
121
|
+
# require 'functional'
|
122
|
+
#
|
123
|
+
# class Factors
|
124
|
+
# include Functional::Memo
|
125
|
+
#
|
126
|
+
# def self.sum_of(number)
|
127
|
+
# of(number).reduce(:+)
|
128
|
+
# end
|
129
|
+
#
|
130
|
+
# def self.of(number)
|
131
|
+
# (1..number).select {|i| factor?(number, i)}
|
132
|
+
# end
|
133
|
+
#
|
134
|
+
# def self.factor?(number, potential)
|
135
|
+
# number % potential == 0
|
136
|
+
# end
|
137
|
+
#
|
138
|
+
# def self.perfect?(number)
|
139
|
+
# sum_of(number) == 2 * number
|
140
|
+
# end
|
141
|
+
#
|
142
|
+
# def self.abundant?(number)
|
143
|
+
# sum_of(number) > 2 * number
|
144
|
+
# end
|
145
|
+
#
|
146
|
+
# def self.deficient?(number)
|
147
|
+
# sum_of(number) < 2 * number
|
148
|
+
# end
|
149
|
+
#
|
150
|
+
# memoize(:sum_of)
|
151
|
+
# memoize(:of)
|
152
|
+
# end
|
153
|
+
# ```
|
154
|
+
#
|
155
|
+
# This code was tested in IRB using MRI 2.1.2 on a MacBook Pro. The `sum_of`
|
156
|
+
# method was called three times against the number 10,000,000 and the
|
157
|
+
# benchmark results of each run were captured. The test code:
|
158
|
+
#
|
159
|
+
# ```ruby
|
160
|
+
# require 'benchmark'
|
161
|
+
#
|
162
|
+
# 3.times do
|
163
|
+
# stats = Benchmark.measure do
|
164
|
+
# Factors.sum_of(10_000_000)
|
165
|
+
# end
|
166
|
+
# puts stats
|
167
|
+
# end
|
168
|
+
# ```
|
169
|
+
#
|
170
|
+
# The results of the benchmarking are very revealing. The first run took over
|
171
|
+
# a second to calculate the results. The two subsequent runs, which retrieved
|
172
|
+
# the previous result from the memo cache, were nearly instantaneous:
|
173
|
+
#
|
174
|
+
# ```
|
175
|
+
# 1.080000 0.000000 1.080000 ( 1.077524)
|
176
|
+
# 0.000000 0.000000 0.000000 ( 0.000033)
|
177
|
+
# 0.000000 0.000000 0.000000 ( 0.000008)
|
178
|
+
# ```
|
179
|
+
#
|
180
|
+
# The same code run on the same computer using JRuby 1.7.12 exhibited similar
|
181
|
+
# results:
|
182
|
+
#
|
183
|
+
# ```
|
184
|
+
# 1.800000 0.030000 1.830000 ( 1.494000)
|
185
|
+
# 0.000000 0.000000 0.000000 ( 0.000000)
|
186
|
+
# 0.000000 0.000000 0.000000 ( 0.000000)
|
187
|
+
# ```
|
188
|
+
#
|
189
|
+
# ## Inspiration
|
190
|
+
#
|
191
|
+
# * [Memoization](http://en.wikipedia.org/wiki/Memoization) at Wikipedia
|
192
|
+
# * Clojure [memoize](http://clojuredocs.org/clojure_core/clojure.core/memoize) function
|