functional-ruby 0.7.7 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/functional-ruby.png)](http://badge.fury.io/rb/functional-ruby) [![Build Status](https://secure.travis-ci.org/jdantonio/functional-ruby.png)](https://travis-ci.org/jdantonio/functional-ruby?branch=master) [![Coverage Status](https://coveralls.io/repos/jdantonio/functional-ruby/badge.png)](https://coveralls.io/r/jdantonio/functional-ruby) [![Code Climate](https://codeclimate.com/github/jdantonio/functional-ruby.png)](https://codeclimate.com/github/jdantonio/functional-ruby) [![Inline docs](http://inch-ci.org/github/jdantonio/functional-ruby.png)](http://inch-ci.org/github/jdantonio/functional-ruby) [![Dependency Status](https://gemnasium.com/jdantonio/functional-ruby.png)](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
|