Showing preview only (286K chars total). Download the full file or copy to clipboard to get everything.
Repository: jdantonio/functional-ruby
Branch: master
Commit: fc99f555f03d
Files: 59
Total size: 269.2 KB
Directory structure:
gitextract_u_5mpmcl/
├── .coveralls.yml
├── .gitignore
├── .rspec
├── .travis.yml
├── .yardopts
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE
├── README.md
├── Rakefile
├── appveyor.yml
├── doc/
│ ├── memo.md
│ ├── memoize.rb
│ ├── pattern_matching.md
│ ├── protocol.md
│ └── record.md
├── functional_ruby.gemspec
├── lib/
│ ├── functional/
│ │ ├── abstract_struct.rb
│ │ ├── delay.rb
│ │ ├── either.rb
│ │ ├── final_struct.rb
│ │ ├── final_var.rb
│ │ ├── memo.rb
│ │ ├── method_signature.rb
│ │ ├── option.rb
│ │ ├── pattern_matching.rb
│ │ ├── protocol.rb
│ │ ├── protocol_info.rb
│ │ ├── record.rb
│ │ ├── synchronization.rb
│ │ ├── tuple.rb
│ │ ├── type_check.rb
│ │ ├── union.rb
│ │ ├── value_struct.rb
│ │ └── version.rb
│ └── functional.rb
├── spec/
│ ├── .gitignore
│ ├── functional/
│ │ ├── abstract_struct_shared.rb
│ │ ├── complex_pattern_matching_spec.rb
│ │ ├── delay_spec.rb
│ │ ├── either_spec.rb
│ │ ├── final_struct_spec.rb
│ │ ├── final_var_spec.rb
│ │ ├── memo_spec.rb
│ │ ├── option_spec.rb
│ │ ├── pattern_matching_spec.rb
│ │ ├── protocol_info_spec.rb
│ │ ├── protocol_spec.rb
│ │ ├── record_spec.rb
│ │ ├── tuple_spec.rb
│ │ ├── type_check_spec.rb
│ │ ├── union_spec.rb
│ │ └── value_struct_spec.rb
│ ├── spec_helper.rb
│ └── support/
│ └── .gitignore
└── tasks/
├── .gitignore
├── metrics.rake
└── update_doc.rake
================================================
FILE CONTENTS
================================================
================================================
FILE: .coveralls.yml
================================================
repo_token: M3JnILwxCIYb4OjWvyxBJkib9xsAGdnek
================================================
FILE: .gitignore
================================================
Gemfile.lock
.rspec-local
*.gem
lib/1.8
lib/1.9
lib/2.0
.rvmrc
.ruby-version
.ruby-gemset
.bundle/*
.yardoc/*
yardoc/*
tmp/*
man/*
*.tmproj
rdoc/*
*.orig
*.BACKUP.*
*.BASE.*
*.LOCAL.*
*.REMOTE.*
git_pull.txt
coverage
critic
.DS_Store
TAGS
tmtags
*.sw?
.idea
.rbx/*
lib/*.bundle
lib/*.so
lib/*.jar
ext/*.bundle
ext/*.so
ext/*.jar
pkg
*.gem
================================================
FILE: .rspec
================================================
--require spec_helper
--format progress
================================================
FILE: .travis.yml
================================================
language: ruby
rvm:
- 2.2.3
- 2.2.2
- 2.2.1
- 2.1.5
- 2.1.4
- 2.0.0
- ruby-head
- jruby-1.7.19
- jruby-9.0.1.0
- jruby-9.0.3.0
- jruby-9.0.4.0
- jruby-head
- rbx-2
jdk:
- oraclejdk8
sudo: false
branches:
only:
- master
matrix:
allow_failures:
- rvm: ruby-head
- rvm: jruby-head
- rvm: jruby-9.0.1.0
- rvm: rbx-2
script: "CODECLIMATE_REPO_TOKEN=65d4787423f734f5cf6d2b3f9be88e481802e50af0879e8ed66971f972d70894 bundle exec rake"
================================================
FILE: .yardopts
================================================
--protected
--no-private
--embed-mixins
--output-dir ./yardoc
--markup markdown
--title=Functional Ruby
--template default
./lib/**/*.rb
-
README.md
CHANGELOG.md
LICENSE
================================================
FILE: CHANGELOG.md
================================================
## Current Release v1.3.0 (October 4, 2015)
* Pattern match now check arity of pattern and block
* `PatternMatching::ALL` pattern now should be presented as variable length args (*args)
* `NoMethodError` and `ArgumentError` raised from method block won't be catched anymore by lib
### Release v1.2.0 (July 10, 2015)
* `Record` classes can be declared with a type/protocol specification for type safety.
* Improved documentation
* Improved tests
* Better synchronization (thread safety) on all platforms
* Continuous integration run on both Linux (Travis CI) and Windows (AppVeyor)
### Release v1.1.0 (August 12, 2014)
* A simple implementation of [tuple](http://en.wikipedia.org/wiki/Tuple), an
immutable, fixed-length list/array/vector-like data structure.
* `FinalStruct`, a variation on Ruby's `OpenStruct` in which all fields are "final" (meaning
that new fields can be arbitrarily added but once set each field becomes immutable).
* `FinalVar`, a thread safe object that holds a single value and is "final" (meaning
that the value can be set at most once after which it becomes immutable).
### Release v1.0.0 (July 30, 2014)
* Protocol specifications inspired by Clojure [protocol](http://clojure.org/protocols),
Erlang [behavior](http://www.erlang.org/doc/design_principles/des_princ.html#id60128),
and Objective-C [protocol](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithProtocols/WorkingwithProtocols.html)
* Function overloading with Erlang-style [function](http://erlang.org/doc/reference_manual/functions.html)
[pattern matching](http://erlang.org/doc/reference_manual/patterns.html)
* Simple, immutable data structures, such as *record* and *union*, inspired by
[Clojure](http://clojure.org/datatypes), [Erlang](http://www.erlang.org/doc/reference_manual/records.html),
and [others](http://en.wikipedia.org/wiki/Union_type)
* `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)
* [Memoization](http://en.wikipedia.org/wiki/Memoization) of class methods based on Clojure [memoize](http://clojuredocs.org/clojure_core/clojure.core/memoize)
* Lazy execution with a `Delay` class based on Clojure [delay](http://clojuredocs.org/clojure_core/clojure.core/delay)
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Code of Conduct
As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing other's private information, such as physical or electronic addresses, without explicit permission
* Other unethical or unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team.
This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)
================================================
FILE: Gemfile
================================================
source 'https://rubygems.org'
gemspec
group :development do
gem 'rake', '~> 12.3.0'
end
group :testing do
gem 'rspec', '~> 3.7.0'
gem 'simplecov', '~> 0.14.1', platforms: :mri, require: false
gem 'coveralls', '~> 0.8.21', require: false
end
group :documentation do
gem 'countloc', '~> 0.4.0', platforms: :mri, require: false
gem 'yard', '~> 0.9.12', require: false
gem 'redcarpet', '~> 3.4.0', platforms: :mri # understands github markdown
end
================================================
FILE: LICENSE
================================================
Copyright (c) Jerry D'Antonio -- released under the MIT license.
http://www.opensource.org/licenses/mit-license.php
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
# Functional Ruby
[](http://badge.fury.io/rb/functional-ruby)
[](https://travis-ci.org/jdantonio/functional-ruby?branch=master)
[](https://ci.appveyor.com/project/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)
[](http://opensource.org/licenses/MIT)
**A gem for adding functional programming tools to Ruby. Inspired by [Erlang](http://www.erlang.org/),
[Clojure](http://clojure.org/), and [Functional Java](http://functionaljava.org/).**
## Introduction
Two things I love are [Ruby](http://www.ruby-lang.org/en/) and
[functional](https://en.wikipedia.org/wiki/Functional_programming)
[programming](http://c2.com/cgi/wiki?FunctionalProgramming).
If you combine Ruby's ability to create functions sans-classes with the power
of blocks, `proc`, and `lambda`, Ruby code can follow just about every modern functional
programming design paradigm. Add to this Ruby's vast metaprogramming capabilities
and Ruby is easily one of the most powerful languages in common use today.
### Goals
Our goal is to implement various functional programming patterns in Ruby. Specifically:
* Be an 'unopinionated' toolbox that provides useful utilities without debating which is better or why
* Remain free of external gem dependencies
* Stay true to the spirit of the languages providing inspiration
* But implement in a way that makes sense for Ruby
* Keep the semantics as idiomatic Ruby as possible
* Support features that make sense in Ruby
* Exclude features that don't make sense in Ruby
* Keep everything small
* Be as fast as reasonably possible
## Features
The primary site for documentation is the automatically generated [API documentation](http://jdantonio.github.io/functional-ruby/).
* Protocol specifications inspired by Clojure [protocol](http://clojure.org/protocols),
Erlang [behavior](http://www.erlang.org/doc/design_principles/des_princ.html#id60128),
and Objective-C [protocol](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithProtocols/WorkingwithProtocols.html).
* Function overloading with Erlang-style [function](http://erlang.org/doc/reference_manual/functions.html)
[pattern matching](http://erlang.org/doc/reference_manual/patterns.html).
* Simple, thread safe, immutable data structures, such as `Record`, `Union`, and `Tuple`, inspired by
[Clojure](http://clojure.org/datatypes), [Erlang](http://www.erlang.org/doc/reference_manual/records.html),
and other functional languages.
* Thread safe, immutable `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).
* [Memoization](http://en.wikipedia.org/wiki/Memoization) of class methods based on Clojure [memoize](http://clojuredocs.org/clojure_core/clojure.core/memoize).
* Lazy execution with a `Delay` class based on Clojure [delay](http://clojuredocs.org/clojure_core/clojure.core/delay).
* `ValueStruct`, a simple, thread safe, immutable variation of Ruby's [OpenStruct](http://ruby-doc.org/stdlib-2.0/libdoc/ostruct/rdoc/OpenStruct.html) class.
* Thread safe data structures, such as `FinalStruct` and `FinalVar`, which can be written to at most once
before becoming immutable. Based on [Java's `final` keyword](http://en.wikipedia.org/wiki/Final_(Java)).
### Supported Ruby Versions
MRI 2.0 and higher, JRuby (1.9 mode), and Rubinius 2.x. This library is pure Ruby and has no gem dependencies.
It should be fully compatible with any interpreter that is compliant with Ruby 2.0 or newer.
### Install
```shell
gem install functional-ruby
```
or add the following line to Gemfile:
```ruby
gem 'functional-ruby'
```
and run `bundle install` from your shell.
Once you've installed the gem you must `require` it in your project:
```ruby
require 'functional'
```
## Examples
Specifying a [protocol](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/Protocol):
```ruby
Functional::SpecifyProtocol(:Name) do
attr_accessor :first
attr_accessor :middle
attr_accessor :last
attr_accessor :suffix
end
```
Defining immutable [data structures](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/AbstractStruct) including
[Either](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/Either),
[Option](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/Option),
[Union](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/Union) and
[Record](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/Record)
```ruby
Name = Functional::Record.new(:first, :middle, :last, :suffix) do
mandatory :first, :last
default :first, 'J.'
default :last, 'Doe'
end
anon = Name.new #=> #<record Name :first=>"J.", :middle=>nil, :last=>"Doe", :suffix=>nil>
matz = Name.new(first: 'Yukihiro', last: 'Matsumoto') #=> #<record Name :first=>"Yukihiro", :middle=>nil, :last=>"Matsumoto", :suffix=>nil>
```
[Pattern matching](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/PatternMatching)
using [protocols](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/Protocol),
[type](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/TypeCheck) checking,
and other options:
```ruby
class Foo
include Functional::PatternMatching
include Functional::Protocol
include Functional::TypeCheck
def greet
return 'Hello, World!'
end
defn(:greet, _) do |name|
"Hello, #{name}!"
end
defn(:greet, _) { |name|
"Pleased to meet you, #{name.full_name}!"
}.when {|name| Type?(name, CustomerModel, ClientModel) }
defn(:greet, _) { |name|
"Hello, #{name.first} #{name.last}!"
}.when {|name| Satisfy?(name, :Name) }
defn(:greet, :doctor, _) { |name|
"Hello, Dr. #{name}!"
}
defn(:greet, nil, _) { |name|
"Goodbye, #{name}!"
}
defn(:greet, _, _) { |_, name|
"Hello, #{name}!"
}
end
```
Performance improvement of idempotent functions through [memoization](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/Memo):
```ruby
class Factors
include Functional::Memo
def self.sum_of(number)
of(number).reduce(:+)
end
def self.of(number)
(1..number).select {|i| factor?(number, i)}
end
def self.factor?(number, potential)
number % potential == 0
end
memoize(:sum_of)
memoize(:of)
end
```
## Contributing
1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request
## License and Copyright
*Functional Ruby* is free software released under the [MIT License](http://www.opensource.org/licenses/MIT).
================================================
FILE: Rakefile
================================================
$:.push File.join(File.dirname(__FILE__), 'lib')
GEMSPEC = Gem::Specification.load('functional-ruby.gemspec')
require 'bundler/gem_tasks'
require 'rspec'
require 'rspec/core/rake_task'
require 'functional'
Bundler::GemHelper.install_tasks
Dir.glob('tasks/**/*.rake').each do|rakefile|
load rakefile
end
RSpec::Core::RakeTask.new(:spec) do |t|
t.rspec_opts = '--color --backtrace --format documentation'
end
RSpec::Core::RakeTask.new(:travis_spec) do |t|
t.rspec_opts = '--tag ~@not_on_travis'
end
task :default => [:travis_spec]
================================================
FILE: appveyor.yml
================================================
install:
- SET PATH=C:\Ruby%ruby_version%\bin;%PATH%
- SET PATH=C:\MinGW\bin;%PATH%
- SET RAKEOPT=-rdevkit
- ruby --version
- gem --version
- bundle install
build: off
test_script:
- bundle exec rake
environment:
matrix:
- ruby_version: "200"
- ruby_version: "200-x64"
- ruby_version: "21"
- ruby_version: "21-x64"
- ruby_version: "22"
- ruby_version: "22-x64"
#matrix:
#allow_failures:
#- ruby_version: "193"
================================================
FILE: doc/memo.md
================================================
# memoize
### Rationale
Many computational operations take a significant amount of time and/or use
an inordinate amount of resources. If subsequent calls to that function with
the same parameters are guaranteed to return the same result, caching the
result can lead to significant performance improvements. The process of
caching such calls is called
[memoization](http://en.wikipedia.org/wiki/Memoization).
### Declaration
Using memoization requires two simple steps: including the
`Functional::Memo` module within a class or module and calling the `memoize`
function to enable memoization on one or more methods.
```ruby
Module EvenNumbers
include Functional::Memoize
self.first(n)
(2..n).select{|i| i % 2 == 0 }
end
memoize :first
end
```
When a function is memoized an internal cache is created that maps arguments
to return values. When the function is called the arguments are checked
against the cache. If the args are found the method is not called and the
cached result is returned instead.
### Ramifications
Memoizing long-running methods can lead to significant performance
advantages. But there is a trade-off. Memoization may greatly increase the
memory footprint of the application. The memo cache itself takes memory. The
more arg/result pairs stored in the cache, the more memory is consumed.
##### Cache Size Options
To help control the size of the cache, a limit can be placed on the number
of items retained in the cache. The `:at_most` option, when given, indicates
the maximum size of the cache. Once the maximum cache size is reached, calls
to to the method with uncached args will still result in the method being
called, but the results will not be cached.
```ruby
Module EvenNumbers
include Functional::Memoize
self.first(n)
(2..n).select{|i| i % 2 == 0 }
end
memoize :first, at_most: 1000
end
```
There is no way to predict in advance what the proper cache size is, or if
it should be restricted at all. Only performance testing under realistic
conditions or profiling of a running system can provide guidance.
### Restrictions
Not all methods are good candidates for memoization.Only methods that are
[idempotent](http://en.wikipedia.org/wiki/Idempotence), [referentially
transparent](http://en.wikipedia.org/wiki/Referential_transparency_(computer_science)),
and free of [side effects](http://en.wikipedia.org/wiki/Side_effect_(computer_science))
can be effectively memoized. If a method creates side effects, such as
writing to a log, only the first call to the method will create those side
effects. Subsequent calls will return the cached value without calling the
method.
Similarly, methods which change internal state will only update the state on
the initial call. Later calls will not result in state changes, they will
only return the original result. Subsequently, instance methods cannot be
memoized. Objects are, by definition, stateful. Method calls exist for the
purpose of changing or using the internal state of the object. Such methods
cannot be effectively memoized; it would require the internal state of the
object to be cached and checked as well.
Block parameters pose a similar problem. Block parameters are inherently
stateful (they are closures which capture the enclosing context). And there
is no way to check the state of the block along with the args to determine
if the cached value should be used. Subsequently, and method call which
includes a block will result in the cache being completely skipped. The base
method will be called and the result will not be cached. This behavior will
occur even when the given method was not programmed to accept a block
parameter. Ruby will capture any block passed to any method and make it
available to the method even when not documented as a formal parameter or
used in the method. This has the interesting side effect of allowing the
memo cache to be skipped on any method call, simply be passing a block
parameter.
```ruby
EvenNumbers.first(100) causes the result to be cached
EvenNumbers.first(100) retrieves the previous result from the cache
EvenNumbers.first(100){ nil } skips the memo cache and calls the method again
```
### Complete Example
The following example is borrowed from the book [Functional Thinking](http://shop.oreilly.com/product/0636920029687.do)
by Neal Ford. In his book he shows an example of memoization in Groovy by
summing factors of a given number. This is a great example because it
exhibits all the criteria that make a method a good memoization candidate:
* Idempotence
* Referential transparency
* Stateless
* Free of side effect
* Computationally expensive (for large numbers)
The following code implements Ford's algorithms in Ruby, then memoizes two
key methods. The Ruby code:
```ruby
require 'functional'
class Factors
include Functional::Memo
def self.sum_of(number)
of(number).reduce(:+)
end
def self.of(number)
(1..number).select {|i| factor?(number, i)}
end
def self.factor?(number, potential)
number % potential == 0
end
def self.perfect?(number)
sum_of(number) == 2 * number
end
def self.abundant?(number)
sum_of(number) > 2 * number
end
def self.deficient?(number)
sum_of(number) < 2 * number
end
memoize(:sum_of)
memoize(:of)
end
```
This code was tested in IRB using MRI 2.1.2 on a MacBook Pro. The `sum_of`
method was called three times against the number 10,000,000 and the
benchmark results of each run were captured. The test code:
```ruby
require 'benchmark'
3.times do
stats = Benchmark.measure do
Factors.sum_of(10_000_000)
end
puts stats
end
```
The results of the benchmarking are very revealing. The first run took over
a second to calculate the results. The two subsequent runs, which retrieved
the previous result from the memo cache, were nearly instantaneous:
```
1.080000 0.000000 1.080000 ( 1.077524)
0.000000 0.000000 0.000000 ( 0.000033)
0.000000 0.000000 0.000000 ( 0.000008)
```
The same code run on the same computer using JRuby 1.7.12 exhibited similar
results:
```
1.800000 0.030000 1.830000 ( 1.494000)
0.000000 0.000000 0.000000 ( 0.000000)
0.000000 0.000000 0.000000 ( 0.000000)
```
### Inspiration
* [Memoization](http://en.wikipedia.org/wiki/Memoization) at Wikipedia
* Clojure [memoize](http://clojuredocs.org/clojure_core/clojure.core/memoize) function
================================================
FILE: doc/memoize.rb
================================================
#!/usr/bin/env ruby
$LOAD_PATH << File.expand_path('../../lib', __FILE__)
require 'functional'
class Factors
include Functional::Memo
def self.sum_of(number)
of(number).reduce(:+)
end
def self.of(number)
(1..number).select {|i| factor?(number, i)}
end
def self.factor?(number, potential)
number % potential == 0
end
def self.perfect?(number)
sum_of(number) == 2 * number
end
def self.abundant?(number)
sum_of(number) > 2 * number
end
def self.deficient?(number)
sum_of(number) < 2 * number
end
memoize(:sum_of)
memoize(:of)
end
require 'benchmark'
require 'pp'
def memory_usage
`ps ax -o pid,rss | grep -E "^[[:space:]]*#{$$}"`.strip.split.map(&:to_i)
end
def print_memory_usage
pid, size = memory_usage
puts "Memory used by process #{pid} at #{Time.now} is #{size}"
end
def run_benchmark(n = 10000)
puts "Benchmarks for #{n} numbers..."
puts
puts 'With no memoization...'
stats = Benchmark.measure do
Factors.sum_of(n)
end
puts stats
2.times do
puts
puts 'With memoization...'
stats = Benchmark.measure do
Factors.sum_of(n)
end
puts stats
end
end
if $0 == __FILE__
run_benchmark(10_000_000)
end
__END__
$ ./doc/memoize.rb
Benchmarks for 10000000 numbers...
With no memoization...
1.660000 0.000000 1.660000 ( 1.657253)
With memoization...
0.000000 0.000000 0.000000 ( 0.000019)
With memoization...
0.000000 0.000000 0.000000 ( 0.000008)
================================================
FILE: doc/pattern_matching.md
================================================
### Features
* Pattern matching for instance methods.
* Pattern matching for object constructors.
* Parameter count matching
* Matching against primitive values
* Matching by class/datatype
* Matching against specific key/vaue pairs in hashes
* Matching against the presence of keys within hashes
* Implicit hash for last parameter
* Variable-length parameter lists
* Guard clauses
* Recursive calls to other pattern matches
* Recursive calls to superclass pattern matches
* Recursive calls to superclass methods
* Dispatching to superclass methods when no match is found
* Reasonable error messages when no match is found
### Usage
First, familiarize yourself with Erlang [pattern matching](http://learnyousomeerlang.com/syntax-in-functionspattern-matching).
This gem may not make much sense if you don't understand how Erlang dispatches functions.
In the Ruby class file where you want to use pattern matching, require the *functional-ruby* gem:
```ruby
require 'functional'
```
Then include `Functional::PatternMatching` in your class:
```ruby
require 'functional'
class Foo
include Functional::PatternMatching
...
end
```
You can then define functions with `defn` instead of the normal *def* statement.
The syntax for `defn` is:
```ruby
defn(:symbol_name_of_function, zero, or, more, parameters) { |block, arguments|
code to execute
}
```
You can then call your new function just like any other:
```ruby
require 'functional/pattern_matching'
class Foo
include Functional::PatternMatching
defn(:hello) {
puts "Hello, World!"
}
end
foo = Foo.new
foo.hello => "Hello, World!"
```
Patterns to match against are included in the parameter list:
```ruby
defn(:greet, :male) {
puts "Hello, sir!"
}
defn(:greet, :female) {
puts "Hello, ma'am!"
}
...
foo.greet(:male) => "Hello, sir!"
foo.greet(:female) => "Hello, ma'am!"
```
If a particular method call can not be matched a *NoMethodError* is thrown with
a reasonably helpful error message:
```ruby
foo.greet(:unknown) => NoMethodError: no method `greet` matching [:unknown] found for class Foo
foo.greet => NoMethodError: no method `greet` matching [] found for class Foo
```
Parameters that are expected to exist but that can take any value are considered
*unbound* parameters. Unbound parameters are specified by the `_` underscore
character or `UNBOUND`:
```ruby
defn(:greet, _) do |name|
"Hello, {name}!"
end
defn(:greet, UNBOUND, UNBOUND) do |first, last|
"Hello, {first} {last}!"
end
...
foo.greet('Jerry') => "Hello, Jerry!"
```
All unbound parameters will be passed to the block in the order they are specified in the definition:
```ruby
defn(:greet, _, _) do |first, last|
"Hello, {first} {last}!"
end
...
foo.greet('Jerry', "D'Antonio") => "Hello, Jerry D'Antonio!"
```
If for some reason you don't care about one or more unbound parameters within
the block you can use the `_` underscore character in the block parameters list
as well:
```ruby
defn(:greet, _, _, _) do |first, _, last|
"Hello, {first} {last}!"
end
...
foo.greet('Jerry', "I'm not going to tell you my middle name!", "D'Antonio") => "Hello, Jerry D'Antonio!"
```
Hash parameters can match against specific keys and either bound or unbound parameters. This allows for
function dispatch by hash parameters without having to dig through the hash:
```ruby
defn(:hashable, {foo: :bar}) { |opts|
:foo_bar
}
defn(:hashable, {foo: _}) { |f|
f
}
...
foo.hashable({foo: :bar}) => :foo_bar
foo.hashable({foo: :baz}) => :baz
```
The Ruby idiom of the final parameter being a hash is also supported:
```ruby
defn(:options, _) { |opts|
opts
}
...
foo.options(bar: :baz, one: 1, many: 2)
```
As is the Ruby idiom of variable-length argument lists. The constant `ALL` as the last parameter
will match one or more arguments and pass them to the block as an array:
```ruby
defn(:baz, Integer, ALL) { |int, args|
[int, args]
}
defn(:baz, ALL) { |args|
args
}
```
Superclass polymorphism is supported as well. If an object cannot match a method
signature it will defer to the parent class:
```ruby
class Bar
def greet
return 'Hello, World!'
end
end
class Foo < Bar
include Functional::PatternMatching
defn(:greet, _) do |name|
"Hello, {name}!"
end
end
...
foo.greet('Jerry') => "Hello, Jerry!"
foo.greet => "Hello, World!"
```
Guard clauses in Erlang are defined with `when` clauses between the parameter list and the function body.
In Ruby, guard clauses are defined by chaining a call to `when` onto the the `defn` call and passing
a block. If the guard clause evaluates to true then the function will match. If the guard evaluates
to false the function will not match and pattern matching will continue:
Erlang:
```erlang
old_enough(X) when X >= 16 -> true;
old_enough(_) -> false.
```
Ruby:
```ruby
defn(:old_enough, _){ |_| true }.when{|x| x >= 16 }
defn(:old_enough, _){ |_| false }
```
##### Order Matters
As with Erlang, the order of pattern matches is significant. Patterns will be matched
*in the order declared* and the first match will be used. If a particular function call
can be matched by more than one pattern, the *first matched pattern* will be used. It
is the programmer's responsibility to ensure patterns are declared in the correct order.
##### Blocks and Procs and Lambdas, oh my!
When using this gem it is critical to remember that `defn` takes a block and
that blocks in Ruby have special rules. There are [plenty](https://www.google.com/search?q=ruby+block+proc+lambda)
of good tutorials on the web explaining [blocks](http://www.robertsosinski.com/2008/12/21/understanding-ruby-blocks-procs-and-lambdas/)
and [Procs](https://coderwall.com/p/_-_mha) and [lambdas](http://railsguru.org/2010/03/learn-ruby-procs-blocks-lambda/)
in Ruby. Please read them. Please don't submit a bug report if you use a
`return` statement within your `defn` and your code blows up with a
[LocalJumpError](http://ruby-doc.org/core-2.0/LocalJumpError.html).
##### Examples
For more examples see the integration tests in *spec/integration_spec.rb*.
Simple Functions
This example is based on [Syntax in defnctions: Pattern Matching](http://learnyousomeerlang.com/syntax-in-defnctions) in [Learn You Some Erlang for Great Good!](http://learnyousomeerlang.com/).
Erlang:
```erlang
greet(male, Name) ->
io:format("Hello, Mr. ~s!", [Name]);
greet(female, Name) ->
io:format("Hello, Mrs. ~s!", [Name]);
greet(_, Name) ->
io:format("Hello, ~s!", [Name]).
```
Ruby:
```ruby
require 'functional/pattern_matching'
class Foo
include Functional::PatternMatching
defn(:greet, _) do |name|
"Hello, {name}!"
end
defn(:greet, :male, _) { |name|
"Hello, Mr. {name}!"
}
defn(:greet, :female, _) { |name|
"Hello, Ms. {name}!"
}
defn(:greet, _, _) { |_, name|
"Hello, {name}!"
}
end
```
##### Simple Functions with Overloading
This example is based on [Syntax in defnctions: Pattern Matching](http://learnyousomeerlang.com/syntax-in-defnctions) in [Learn You Some Erlang for Great Good!](http://learnyousomeerlang.com/).
Erlang:
```erlang
greet(Name) ->
io:format("Hello, ~s!", [Name]).
greet(male, Name) ->
io:format("Hello, Mr. ~s!", [Name]);
greet(female, Name) ->
io:format("Hello, Mrs. ~s!", [Name]);
greet(_, Name) ->
io:format("Hello, ~s!", [Name]).
```
Ruby:
```ruby
require 'functional/pattern_matching'
class Foo
include Functional::PatternMatching
defn(:greet, _) do |name|
"Hello, {name}!"
end
defn(:greet, :male, _) { |name|
"Hello, Mr. {name}!"
}
defn(:greet, :female, _) { |name|
"Hello, Ms. {name}!"
}
defn(:greet, nil, _) { |name|
"Goodbye, {name}!"
}
defn(:greet, _, _) { |_, name|
"Hello, {name}!"
}
end
```
Constructor Overloading
```ruby
require 'functional/pattern_matching'
class Foo
include Functional::PatternMatching
defn(:initialize) { @name = 'baz' }
defn(:initialize, _) {|name| @name = name.to_s }
end
```
Matching by Class/Datatype
```ruby
require 'functional/pattern_matching'
class Foo
include Functional::PatternMatching
defn(:concat, Integer, Integer) { |first, second|
first + second
}
defn(:concat, Integer, String) { |first, second|
"{first} {second}"
}
defn(:concat, String, String) { |first, second|
first + second
}
defn(:concat, Integer, _) { |first, second|
first + second.to_i
}
end
```
Matching a Hash Parameter
```ruby
require 'functional/pattern_matching'
class Foo
include Functional::PatternMatching
defn(:hashable, {foo: :bar}) { |opts|
matches any hash with key :foo and value :bar
:foo_bar
}
defn(:hashable, {foo: _, bar: _}) { |f, b|
matches any hash with keys :foo and :bar
passes the values associated with those keys to the block
[f, b]
}
defn(:hashable, {foo: _}) { |f|
matches any hash with key :foo
passes the value associated with that key to the block
must appear AFTER the prior match or it will override that one
f
}
defn(:hashable, {}) { |_|
matches an empty hash
:empty
}
defn(:hashable, _) { |opts|
matches any hash (or any other value)
opts
}
end
...
foo.hashable({foo: :bar}) => :foo_bar
foo.hashable({foo: :baz}) => :baz
foo.hashable({foo: 1, bar: 2}) => [1, 2]
foo.hashable({foo: 1, baz: 2}) => 1
foo.hashable({bar: :baz}) => {bar: :baz}
foo.hashable({}) => :empty
```
Variable Length Argument Lists with ALL
```ruby
defn(:all, :one, ALL) { |args|
args
}
defn(:all, :one, Integer, ALL) { |int, args|
[int, args]
}
defn(:all, 1, _, ALL) { |var, _, *args|
[var, args]
}
defn(:all, ALL) { |*args|
args
}
...
foo.all(:one, 'a', 'bee', :see) => ['a', 'bee', :see]
foo.all(:one, 1, 'bee', :see) => [1, 'bee', :see]
foo.all(1, 'a', 'bee', :see) => ['a', ['bee', :see]]
foo.all('a', 'bee', :see) => ['a', 'bee', :see]
foo.all() => NoMethodError: no method `all` matching [] found for class Foo
```
##### Guard Clauses
These examples are based on [Syntax in defnctions: Pattern Matching](http://learnyousomeerlang.com/syntax-in-defnctions)
in [Learn You Some Erlang for Great Good!](http://learnyousomeerlang.com/).
Erlang:
```erlang
old_enough(X) when X >= 16 -> true;
old_enough(_) -> false.
right_age(X) when X >= 16, X =< 104 ->
true;
right_age(_) ->
false.
wrong_age(X) when X < 16; X > 104 ->
true;
wrong_age(_) ->
false.
```
```ruby
defn(:old_enough, _){ |_| true }.when{|x| x >= 16 }
defn(:old_enough, _){ |_| false }
defn(:right_age, _) { |_|
true
}.when{|x| x >= 16 && x <= 104 }
defn(:right_age, _) { |_|
false
}
defn(:wrong_age, _) { |_|
false
}.when{|x| x < 16 || x > 104 }
defn(:wrong_age, _) { |_|
true
}
```
### Inspiration
Pattern matching has its roots in logic programming languages such as
[Prolog](http://en.wikipedia.org/wiki/Prolog). Pattern matching is a core
feature of the [Erlang](http://www.erlang.org/) programming language. A few
helpful resources are:
* Erlang [modules](http://erlang.org/doc/reference_manual/modules.html)
* Erlang [pattern matching](http://erlang.org/doc/reference_manual/patterns.html)
================================================
FILE: doc/protocol.md
================================================
### Rationale
Traditional object orientation implements polymorphism inheritance. The *Is-A*
relationship indicates that one object "is a" instance of another object.
Implicit in this relationship, however, is the concept of [type](http://en.wikipedia.org/wiki/Data_type).
Every Ruby object has a *type*, and that type is the name of its `Class` or
`Module`. The Ruby runtime provides a number of reflective methods that allow
objects to be interrogated for type information. The principal of thses is the
`is_a?` (alias `kind_of`) method defined in class `Object`.
Unlike many traditional object oriented languages, Ruby is a [dynamically typed](http://en.wikipedia.org/wiki/Dynamic_typingDYNAMIC)
language. Types exist but the runtime is free to cast one type into another
at any time. Moreover, Ruby is a [duck typed](http://en.wikipedia.org/wiki/Duck_typing).
If an object "walks like a duck and quacks like a duck then it must be a duck."
When a method needs called on an object Ruby does not check the type of the object,
it simply checks to see if the requested function exists with the proper
[arity](http://en.wikipedia.org/wiki/Arity) and, if it does, dispatches the call.
The duck type analogue to `is_a?` is `respond_to?`. Thus an object can be interrogated
for its behavior rather than its type.
Although Ruby offers several methods for reflecting on the behavior of a module/class/object,
such as `method`, `instance_methods`, `const_defined?`, the aforementioned `respond_to?`,
and others, Ruby lacks a convenient way to group collections of methods in any way that
does not involve type. Both modules and classes provide mechanisms for combining
methods into cohesive abstractions, but they both imply type. This is anathema to Ruby's
dynamism and duck typing. What Ruby needs is a way to collect a group of method names
and signatures into a cohesive collection that embraces duck typing and dynamic dispatch.
This is what protocols do.
### Specifying
A "protocol" is a loose collection of method, attribute, and constant names with optional
arity values. The protocol definition does very little on its own. The power of protocols
is that they provide a way for modules, classes, and objects to be interrogated with
respect to common behavior, not common type. At the core a protocol is nothing more
than a collection of `respond_to?` method calls that ask the question "Does this thing
*behave* like this other thing."
Protocols are specified with the `Functional::SpecifyProtocol` method. It takes one parameter,
the name of the protocol, and a block which contains the protocol specification. This registers
the protocol specification and makes it available for use later when interrogating ojects
for their behavior.
##### Defining Attributes, Methods, and Constants
A single protocol specification can include definition for attributes, methods,
and constants. Methods and attributes can be defined as class/module methods or
as instance methods. Within the a protocol specification each item must include
the symbolic name of the item being defined.
```ruby
Functional::SpecifyProtocol(:KitchenSink) do
instance_method :instance_method
class_method :class_method
attr_accessor :attr_accessor
attr_reader :attr_reader
attr_writer :attr_writer
class_attr_accessor :class_attr_accessor
class_attr_reader :class_attr_reader
class_attr_writer :class_attr_writer
constant :CONSTANT
end
```
Definitions for accessors are expanded at specification into the apprporiate
method(s). Which means that this:
```ruby
Functional::SpecifyProtocol(:Name) do
attr_accessor :first
attr_accessor :middle
attr_accessor :last
attr_accessor :suffix
end
```
is the same as:
```ruby
Functional::SpecifyProtocol(:Name) do
instance_method :first
instance_method :first=
instance_method :middle
instance_method :middle=
instance_method :last
instance_method :last=
instance_method :suffix
instance_method :suffix=
end
```
Protocols only care about the methods themselves, not how they were declared.
### Arity
In addition to defining *which* methods exist, the required method arity can
indicated. Arity is optional. When no arity is given any arity will be expected.
The arity rules follow those defined for the `arity` method of Ruby's
[Method class](http://www.ruby-doc.org/core-2.1.2/Method.htmlmethod-i-arity):
* Methods with a fixed number of arguments have a non-negative arity
* Methods with optional arguments have an arity `-n - 1`, where n is the number of required arguments
* Methods with a variable number of arguments have an arity of `-1`
```ruby
Functional::SpecifyProtocol(:Foo) do
instance_method :any_args
instance_method :no_args, 0
instance_method :three_args, 3
instance_method :optional_args, -2
instance_method :variable_args, -1
end
class Bar
def any_args(a, b, c=1, d=2, *args)
end
def no_args
end
def three_args(a, b, c)
end
def optional_args(a, b=1, c=2)
end
def variable_args(*args)
end
end
```
### Reflection
Once a protocol has been defined, any class, method, or object may be interrogated
for adherence to one or more protocol specifications. The methods of the
`Functional::Protocol` classes provide this capability. The `Satisfy?` method
takes a module/class/object as the first parameter and one or more protocol names
as the second and subsequent parameters. It returns a boolean value indicating
whether the given object satisfies the protocol requirements:
```ruby
Functional::SpecifyProtocol(:Queue) do
instance_method :push, 1
instance_method :pop, 0
instance_method :length, 0
end
Functional::SpecifyProtocol(:List) do
instance_method :[]=, 2
instance_method :[], 1
instance_method :each, 0
instance_method :length, 0
end
Functional::Protocol::Satisfy?(Queue, :Queue) => true
Functional::Protocol::Satisfy?(Queue, :List) => false
list = [1, 2, 3]
Functional::Protocol::Satisfy?(Array, :List, :Queue) => true
Functional::Protocol::Satisfy?(list, :List, :Queue) => true
Functional::Protocol::Satisfy?(Hash, :Queue) => false
Functional::Protocol::Satisfy?('foo bar baz', :List) => false
```
The `Satisfy!` method performs the exact same check but instead raises an exception
when the protocol is not satisfied:
```
2.1.2 :021 > Functional::Protocol::Satisfy!(Queue, :List)
Functional::ProtocolError: Value (Class) 'Thread::Queue' does not behave as all of: :List.
from /Projects/functional-ruby/lib/functional/protocol.rb:67:in `error'
from /Projects/functional-ruby/lib/functional/protocol.rb:36:in `Satisfy!'
from (irb):21
...
```
The `Functional::Protocol` module can be included within other classes
to eliminate the namespace requirement when calling:
```ruby
class MessageFormatter
include Functional::Protocol
def format(message)
if Satisfy?(message, :Internal)
format_internal_message(message)
elsif Satisfy?(message, :Error)
format_error_message(message)
else
format_generic_message(message)
end
end
private
def format_internal_message(message)
format the message...
end
def format_error_message(message)
format the message...
end
def format_generic_message(message)
format the message...
end
```
### Inspiration
Protocols and similar functionality exist in several other programming languages.
A few languages that provided inspiration for this inplementation are:
* Clojure [protocol](http://clojure.org/protocols)
* Erlang [behaviours](http://www.erlang.org/doc/design_principles/des_princ.htmlid60128)
* Objective-C [protocol](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithProtocols/WorkingwithProtocols.html)
(and the corresponding Swift [protocol](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html))
================================================
FILE: doc/record.md
================================================
### Declaration
A `Record` class is declared in a manner identical to that used with Ruby's `Struct`.
The class method `new` is called with a list of one or more field names (symbols).
A new class will then be dynamically generated along with the necessary reader
attributes, one for each field. The newly created class will be anonymous and
will mixin `Functional::AbstractStruct`. The best practice is to assign the newly
created record class to a constant:
```ruby
Customer = Functional::Record.new(:name, :address) => Customer
```
Alternatively, the name of the record class, as a string, can be given as the
first parameter. In this case the new record class will be created as a constant
within the `Record` module:
```ruby
Functional::Record.new("Customer", :name, :address) => Functional::Record::Customer
```
### Type Specification
Unlike a Ruby `Struct`, a `Record` may be declared with a type/protocol
specification. In this case, all data members are checked against the
specification whenever a new record is created. Declaring a `Record` with a
type specification is similar to declaring a normal `Record`, except that
the field list is given as a hash with field names as the keys and a class or
protocol as the values.
```ruby
Functional::SpecifyProtocol(:Name) do
attr_reader :first
attr_reader :middle
attr_reader :last
end
TypedCustomer = Functional::Record.new(name: :Name, address: String) => TypedCustomer
Functional::Record.new("TypedCustomer", name: :Name, address: String) => Functional::Record::TypedCustomer
```
### Construction
Construction of a new object from a record is slightly different than for a Ruby `Struct`.
The constructor for a struct class may take zero or more field values and will use those
values to popuate the fields. The values passed to the constructor are assumed to be in
the same order as the fields were defined. This works for a struct because it is
mutable--the field values may be changed after instanciation. Therefore it is not
necessary to provide all values to a stuct at creation. This is not the case for a
record. A record is immutable. The values for all its fields must be set at instanciation
because they cannot be changed later. When creating a new record object the constructor
will accept a collection of field/value pairs in hash syntax and will create the new
record with the given values:
```ruby
Customer.new(name: 'Dave', address: '123 Main')
=> <record Customer :name=>"Dave", :address=>"123 Main">
Functional::Record::Customer.new(name: 'Dave', address: '123 Main')
=> <record Functional::Record::Customer :name=>"Dave", :address=>"123 Main">
```
When a record is defined with a type/protocol specification, the values of
all non-nil data members are checked against the specification. Any data
value that is not of the given type or does not satisfy the given protocol
will cause an exception to be raised:
```ruby
class Name
attr_reader :first, :middle, :last
def initialize(first, middle, last)
@first = first
@middle = middle
@last = last
end
end
name = Name.new('Douglas', nil, 'Adams') => <Name:0x007fc8b951a278 ...
TypedCustomer.new(name: name, address: '123 Main') => <record TypedCustomer :name=><Name:0x007f914cce05b0 ...
TypedCustomer.new(name: 'Douglas Adams', address: '123 Main') => ArgumentError: 'name' must stasify the protocol :Name
TypedCustomer.new(name: name, address: 42) => ArgumentError: 'address' must be of type String
```
### Default Values
By default, all record fields are set to `nil` at instanciation unless explicity set
via the constructor. It is possible to specify default values other than `nil` for
zero or more of the fields when a new record class is created. The `new` method of
`Record` accepts a block which can be used to declare new default values:
```ruby
Address = Functional::Record.new(:street_line_1, :street_line_2,
:city, :state, :postal_code, :country) do
default :state, 'Ohio'
default :country, 'USA'
end
=> Address
```
When a new object is created from a record class with explicit default values, those
values will be used for the appropriate fields when no other value is given at
construction:
```ruby
Address.new(street_line_1: '2401 Ontario St',
city: 'Cleveland', postal_code: 44115)
=> <record Address :street_line_1=>"2401 Ontario St", :street_line_2=>nil, :city=>"Cleveland", :state=>"Ohio", :postal_code=>44115, :country=>"USA">
```
Of course, if a value for a field is given at construction that value will be used instead
of the custom default:
```ruby
Address.new(street_line_1: '1060 W Addison St',
city: 'Chicago', state: 'Illinois', postal_code: 60613)
=> <record Address :street_line_1=>"1060 W Addison St", :street_line_2=>nil, :city=>"Chicago", :state=>"Illinois", :postal_code=>60613, :country=>"USA">
```
### Mandatory Fields
By default, all record fields are optional. It is perfectly legal for a record
object to exist with all its fields set to `nil`. During declaration of a new record
class the block passed to `Record.new` can also be used to indicate which fields
are mandatory. When a new object is created from a record with mandatory fields
an exception will be thrown if any of those fields are nil:
```ruby
Name = Functional::Record.new(:first, :middle, :last, :suffix) do
mandatory :first, :last
end
=> Name
Name.new(first: 'Joe', last: 'Armstrong')
=> <record Name :first=>"Joe", :middle=>nil, :last=>"Armstrong", :suffix=>nil>
Name.new(first: 'Matz') => ArgumentError: mandatory fields must not be nil
```
Of course, declarations for default values and mandatory fields may be used
together:
```ruby
Person = Functional::Record.new(:first_name, :middle_name, :last_name,
:street_line_1, :street_line_2,
:city, :state, :postal_code, :country) do
mandatory :first_name, :last_name
mandatory :country
default :state, 'Ohio'
default :country, 'USA'
end
=> Person
```
### Default Value Memoization
Note that the block provided to `Record.new` is processed once and only once
when the new record class is declared. Thereafter the results are memoized
and copied (via `clone`, unless uncloneable) each time a new record object
is created. Default values should be simple types like `String`, `Fixnum`,
and `Boolean`. If complex operations need performed when setting default
values the a `Class` should be used instead of a `Record`.
##### Why Declaration Differs from Ruby's Struct
Those familiar with Ruby's `Struct` class will notice one important
difference when declaring a `Record`: the block passes to `new` cannot be
used to define additional methods. When declaring a new class created from a
Ruby `Struct` the block can perform any additional class definition that
could be done had the class be defined normally. The excellent
[Values](https://github.com/tcrayford/Values) supports this same behavior.
`Record` does not allow additional class definitions during declaration for
one simple reason: doing so violates two very important tenets of functional
programming. Specifically, immutability and the separation of data from
operations.
`Record` exists for the purpose of creating immutable objects. If additional
instance methods were to be defined on a record class it would be possible
to violate immutability. Not only could additional, mutable state be added
to the class, but the existing immutable attributes could be overridden by
mutable methods. The security of providing an immutable object would be
completely shattered, thus defeating the original purpose of the record
class. Of course it would be possible to allow this feature and trust the
programmer to not violate the intended immutability of class, but opening
`Record` to the *possibility* of immutability violation is unnecessary and
unwise.
More important than the potential for immutability violations is the fact
the adding additional methods to a record violates the principal of
separating data from operations on that data. This is one of the core ideas
in functional programming. Data is defined in pure structures that contain
no behavior and operations on that data are provided by polymorphic
functions. This may seem counterintuitive to object oriented programmers,
but that is the nature of functional programming. Adding behavior to a
record, even when that behavior does not violate immutability, is still
anathema to functional programming, and it is why records in languages like
Erlang and Clojure do not have functions defined within them.
Should additional methods need defined on a `Record` class, the appropriate
practice is to declare the record class then declare another class which
extends the record. The record class remains pure data and the subclass
contains additional operations on that data.
```ruby
NameRecord = Functional::Record.new(:first, :middle, :last, :suffix) do
mandatory :first, :last
end
class Name < NameRecord
def full_name
"{first} {last}"
end
def formal_name
name = [first, middle, last].select{|s| ! s.to_s.empty?}.join(' ')
suffix.to_s.empty? ? name : name + ", {suffix}"
end
end
jerry = Name.new(first: 'Jerry', last: "D'Antonio")
ted = Name.new(first: 'Ted', middle: 'Theodore', last: 'Logan', suffix: 'Esq.')
jerry.formal_name => "Jerry D'Antonio"
ted.formal_name => "Ted Theodore Logan, Esq."
```
### Inspiration
Neither struct nor records are new to computing. Both have been around for a very
long time. Mutable structs can be found in many languages including
[Ruby](http://www.ruby-doc.org/core-2.1.2/Struct.html),
[Go](http://golang.org/ref/specStruct_types),
[C](http://en.wikipedia.org/wiki/Struct_(C_programming_language)),
and [C](http://msdn.microsoft.com/en-us/library/ah19swz4.aspx),
just to name a few. Immutable records exist primarily in functional languages
like [Haskell](http://en.wikibooks.org/wiki/Haskell/More_on_datatypesNamed_Fields_.28Record_Syntax.29),
Clojure, and Erlang. The inspiration for declaring records with a type
specification is taken from [PureScript](http://www.purescript.org/), a
compile-to-JavaScript language inspired by Haskell.
* [Ruby Struct](http://www.ruby-doc.org/core-2.1.2/Struct.html)
* [Clojure Datatypes](http://clojure.org/datatypes)
* [Clojure *defrecord* macro](http://clojure.github.io/clojure/clojure.core-api.htmlclojure.core/defrecord)
* [Erlang Records (Reference)](http://www.erlang.org/doc/reference_manual/records.html)
* [Erlang Records (Examples)](http://www.erlang.org/doc/programming_examples/records.html)
* [PureScript Records](http://docs.purescript.org/en/latest/types.htmlrecords)
================================================
FILE: functional_ruby.gemspec
================================================
$LOAD_PATH << File.expand_path('../lib', __FILE__)
require 'functional/version'
Gem::Specification.new do |s|
s.name = 'functional-ruby'
s.version = Functional::VERSION
s.platform = Gem::Platform::RUBY
s.author = "Jerry D'Antonio"
s.email = 'jerry.dantonio@gmail.com'
s.homepage = 'https://github.com/jdantonio/functional-ruby/'
s.summary = 'Erlang, Clojure, Haskell, and Functional Java inspired functional programming tools for Ruby.'
s.license = 'MIT'
s.date = Time.now.strftime('%Y-%m-%d')
s.description = <<-EOF
A gem for adding functional programming tools to Ruby. Inspired by Erlang, Clojure, Haskell, and Functional Java.
EOF
s.files = Dir['README*', 'LICENSE*', 'CHANGELOG*']
s.files += Dir['{lib}/**/*']
s.test_files = Dir['{spec}/**/*']
s.extra_rdoc_files = Dir['README*', 'LICENSE*', 'CHANGELOG*']
s.extra_rdoc_files += Dir['{doc}/**/*.{txt,md}']
s.require_paths = ['lib']
s.required_ruby_version = '>= 2.0.0'
end
================================================
FILE: lib/functional/abstract_struct.rb
================================================
require 'functional/protocol'
require 'functional/synchronization'
Functional::SpecifyProtocol(:Struct) do
instance_method :fields
instance_method :values
instance_method :length
instance_method :each
instance_method :each_pair
end
module Functional
# An abstract base class for immutable struct classes.
# @!visibility private
module AbstractStruct
# @return [Array] the values of all record fields in order, frozen
attr_reader :values
# Yields the value of each record field in order.
# If no block is given an enumerator is returned.
#
# @yieldparam [Object] value the value of the given field
#
# @return [Enumerable] when no block is given
def each
return enum_for(:each) unless block_given?
fields.each do |field|
yield(self.send(field))
end
end
# Yields the name and value of each record field in order.
# If no block is given an enumerator is returned.
#
# @yieldparam [Symbol] field the record field for the current iteration
# @yieldparam [Object] value the value of the current field
#
# @return [Enumerable] when no block is given
def each_pair
return enum_for(:each_pair) unless block_given?
fields.each do |field|
yield(field, self.send(field))
end
end
# Equality--Returns `true` if `other` has the same record subclass and has equal
# field values (according to `Object#==`).
#
# @param [Object] other the other record to compare for equality
# @return [Boolean] true when equal else false
def eql?(other)
self.class == other.class && self.to_h == other.to_h
end
alias_method :==, :eql?
# @!macro [attach] inspect_method
#
# Describe the contents of this struct in a string. Will include the name of the
# record class, all fields, and all values.
#
# @return [String] the class and contents of this record
def inspect
state = to_h.to_s.gsub(/^{/, '').gsub(/}$/, '')
"#<#{self.class.datatype} #{self.class} #{state}>"
end
alias_method :to_s, :inspect
# Returns the number of record fields.
#
# @return [Fixnum] the number of record fields
def length
fields.length
end
alias_method :size, :length
# A frozen array of all record fields.
#
# @return [Array] all record fields in order, frozen
def fields
self.class.fields
end
# Returns a Hash containing the names and values for the record’s fields.
#
# @return [Hash] collection of all fields and their associated values
def to_h
@data
end
protected
# Set the internal data hash to a copy of the given hash and freeze it.
# @param [Hash] data the data hash
#
# @!visibility private
def set_data_hash(data)
@data = data.dup.freeze
end
# Set the internal values array to a copy of the given array and freeze it.
# @param [Array] values the values array
#
# @!visibility private
def set_values_array(values)
@values = values.dup.freeze
end
# Define a new struct class and, if necessary, register it with
# the calling class/module. Will also set the datatype and fields
# class attributes on the new struct class.
#
# @param [Module] parent the class/module that is defining the new struct
# @param [Symbol] datatype the datatype value for the new struct class
# @param [Array] fields the list of symbolic names for all data fields
# @return [Functional::AbstractStruct, Array] the new class and the
# (possibly) updated fields array
#
# @!visibility private
def self.define_class(parent, datatype, fields)
struct = Class.new(Functional::Synchronization::Object){ include AbstractStruct }
if fields.first.is_a? String
parent.const_set(fields.first, struct)
fields = fields[1, fields.length-1]
end
fields = fields.collect{|field| field.to_sym }.freeze
struct.send(:datatype=, datatype.to_sym)
struct.send(:fields=, fields)
[struct, fields]
end
private
def self.included(base)
base.extend(ClassMethods)
super(base)
end
# Class methods added to a class that includes {Functional::PatternMatching}
#
# @!visibility private
module ClassMethods
# A frozen Array of all record fields in order
attr_reader :fields
# A symbol describing the object's datatype
attr_reader :datatype
private
# A frozen Array of all record fields in order
attr_writer :fields
# A symbol describing the object's datatype
attr_writer :datatype
fields = [].freeze
datatype = :struct
end
end
end
================================================
FILE: lib/functional/delay.rb
================================================
require 'functional/synchronization'
module Functional
# Lazy evaluation of a block yielding an immutable result. Useful for
# expensive operations that may never be needed.
#
# When a `Delay` is created its state is set to `pending`. The value and
# reason are both `nil`. The first time the `#value` method is called the
# enclosed opration will be run and the calling thread will block. Other
# threads attempting to call `#value` will block as well. Once the operation
# is complete the *value* will be set to the result of the operation or the
# *reason* will be set to the raised exception, as appropriate. All threads
# blocked on `#value` will return. Subsequent calls to `#value` will
# immediately return the cached value. The operation will only be run once.
# This means that any side effects created by the operation will only happen
# once as well.
#
# @!macro [new] thread_safe_immutable_object
#
# @note This is a write-once, read-many, thread safe object that can be
# used in concurrent systems. Thread safety guarantees *cannot* be made
# about objects contained *within* this object, however. Ruby variables
# are mutable references to mutable objects. This cannot be changed. The
# best practice it to only encapsulate immutable, frozen, or thread safe
# objects. Ultimately, thread safety is the responsibility of the
# programmer.
#
# @see http://clojuredocs.org/clojure_core/clojure.core/delay Clojure delay
class Delay < Synchronization::Object
# Create a new `Delay` in the `:pending` state.
#
# @yield the delayed operation to perform
#
# @raise [ArgumentError] if no block is given
def initialize(&block)
raise ArgumentError.new('no block given') unless block_given?
super
synchronize do
@state = :pending
@task = block
end
end
# Current state of block processing.
#
# @return [Symbol] the current state of block processing
def state
synchronize{ @state }
end
# The exception raised when processing the block. Returns `nil` if the
# operation is still `:pending` or has been `:fulfilled`.
#
# @return [StandardError] the exception raised when processing the block
# else nil.
def reason
synchronize{ @reason }
end
# Return the (possibly memoized) value of the delayed operation.
#
# If the state is `:pending` then the calling thread will block while the
# operation is performed. All other threads simultaneously calling `#value`
# will block as well. Once the operation is complete (either `:fulfilled` or
# `:rejected`) all waiting threads will unblock and the new value will be
# returned.
#
# If the state is not `:pending` when `#value` is called the (possibly
# memoized) value will be returned without blocking and without performing
# the operation again.
#
# @return [Object] the (possibly memoized) result of the block operation
def value
synchronize{ execute_task_once }
end
# Has the delay been fulfilled?
# @return [Boolean]
def fulfilled?
synchronize{ @state == :fulfilled }
end
alias_method :value?, :fulfilled?
# Has the delay been rejected?
# @return [Boolean]
def rejected?
synchronize{ @state == :rejected }
end
alias_method :reason?, :rejected?
# Is delay completion still pending?
# @return [Boolean]
def pending?
synchronize{ @state == :pending }
end
protected
# @!visibility private
#
# Execute the enclosed task then cache and return the result if the current
# state is pending. Otherwise, return the cached result.
#
# @return [Object] the result of the block operation
def execute_task_once
if @state == :pending
begin
@value = @task.call
@state = :fulfilled
rescue => ex
@reason = ex
@state = :rejected
end
end
@value
end
end
end
================================================
FILE: lib/functional/either.rb
================================================
require 'functional/abstract_struct'
require 'functional/protocol'
require 'functional/synchronization'
Functional::SpecifyProtocol(:Either) do
instance_method :left, 0
instance_method :left?, 0
instance_method :right, 0
instance_method :right?, 0
end
module Functional
# The `Either` type represents a value of one of two possible types (a
# disjoint union). It is an immutable structure that contains one and only one
# value. That value can be stored in one of two virtual position, `left` or
# `right`. The position provides context for the encapsulated data.
#
# One of the main uses of `Either` is as a return value that can indicate
# either success or failure. Object oriented programs generally report errors
# through either state or exception handling, neither of which work well in
# functional programming. In the former case, a method is called on an object
# and when an error occurs the state of the object is updated to reflect the
# error. This does not translate well to functional programming because they
# eschew state and mutable objects. In the latter, an exception handling block
# provides branching logic when an exception is thrown. This does not
# translate well to functional programming because it eschews side effects
# like structured exception handling (and structured exception handling tends
# to be very expensive). `Either` provides a powerful and easy-to-use
# alternative.
#
# A function that may generate an error can choose to return an immutable
# `Either` object in which the position of the value (left or right) indicates
# the nature of the data. By convention, a `left` value indicates an error and
# a `right` value indicates success. This leaves the caller with no ambiguity
# regarding success or failure, requires no persistent state, and does not
# require expensive exception handling facilities.
#
# `Either` provides several aliases and convenience functions to facilitate
# these failure/success conventions. The `left` and `right` functions,
# including their derivatives, are mirrored by `reason` and `value`. Failure
# is indicated by the presence of a `reason` and success is indicated by the
# presence of a `value`. When an operation has failed the either is in a
# `rejected` state, and when an operation has successed the either is in a
# `fulfilled` state. A common convention is to use a Ruby `Exception` as the
# `reason`. The factory method `error` facilitates this. The semantics and
# conventions of `reason`, `value`, and their derivatives follow the
# conventions of the Concurrent Ruby gem.
#
# The `left`/`right` and `reason`/`value` methods are not mutually exclusive.
# They can be commingled and still result in functionally correct code. This
# practice should be avoided, however. Consistent use of either `left`/`right`
# or `reason`/`value` against each `Either` instance will result in more
# expressive, intent-revealing code.
#
# @example
#
# require 'uri'
#
# def web_host(url)
# uri = URI(url)
# if uri.scheme != 'http'
# Functional::Either.left('Invalid HTTP URL')
# else
# Functional::Either.right(uri.host)
# end
# end
#
# good = web_host('http://www.concurrent-ruby.com')
# good.right? #=> true
# good.right #=> "www.concurrent-ruby"
# good.left #=> nil
#
# good = web_host('bogus')
# good.right? #=> false
# good.right #=> nil
# good.left #=> "Invalid HTTP URL"
#
# @see http://functionaljava.googlecode.com/svn/artifacts/3.0/javadoc/fj/data/Either.html Functional Java
# @see https://hackage.haskell.org/package/base-4.2.0.1/docs/Data-Either.html Haskell Data.Either
# @see http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Obligation.html Concurrent Ruby
#
# @!macro thread_safe_immutable_object
class Either < Synchronization::Object
include AbstractStruct
self.datatype = :either
self.fields = [:left, :right].freeze
# @!visibility private
NO_VALUE = Object.new.freeze
private_class_method :new
class << self
# Construct a left value of either.
#
# @param [Object] value The value underlying the either.
# @return [Either] A new either with the given left value.
def left(value)
new(value, true).freeze
end
alias_method :reason, :left
# Construct a right value of either.
#
# @param [Object] value The value underlying the either.
# @return [Either] A new either with the given right value.
def right(value)
new(value, false).freeze
end
alias_method :value, :right
# Create an `Either` with the left value set to an `Exception` object
# complete with message and backtrace. This is a convenience method for
# supporting the reason/value convention with the reason always being
# an `Exception` object. When no exception class is given `StandardError`
# will be used. When no message is given the default message for the
# given error class will be used.
#
# @example
#
# either = Functional::Either.error("You're a bad monkey, Mojo Jojo")
# either.fulfilled? #=> false
# either.rejected? #=> true
# either.value #=> nil
# either.reason #=> #<StandardError: You're a bad monkey, Mojo Jojo>
#
# @param [String] message The message for the new error object.
# @param [Exception] clazz The class for the new error object.
# @return [Either] A new either with an error object as the left value.
def error(message = nil, clazz = StandardError)
ex = clazz.new(message)
ex.set_backtrace(caller)
left(ex)
end
end
# Projects this either as a left.
#
# @return [Object] The left value or `nil` when `right`.
def left
left? ? to_h[:left] : nil
end
alias_method :reason, :left
# Projects this either as a right.
#
# @return [Object] The right value or `nil` when `left`.
def right
right? ? to_h[:right] : nil
end
alias_method :value, :right
# Returns true if this either is a left, false otherwise.
#
# @return [Boolean] `true` if this either is a left, `false` otherwise.
def left?
@is_left
end
alias_method :reason?, :left?
alias_method :rejected?, :left?
# Returns true if this either is a right, false otherwise.
#
# @return [Boolean] `true` if this either is a right, `false` otherwise.
def right?
! left?
end
alias_method :value?, :right?
alias_method :fulfilled?, :right?
# If this is a left, then return the left value in right, or vice versa.
#
# @return [Either] The value of this either swapped to the opposing side.
def swap
if left?
self.class.send(:new, left, false)
else
self.class.send(:new, right, true)
end
end
# The catamorphism for either. Folds over this either breaking into left or right.
#
# @param [Proc] lproc The function to call if this is left.
# @param [Proc] rproc The function to call if this is right.
# @return [Object] The reduced value.
def either(lproc, rproc)
left? ? lproc.call(left) : rproc.call(right)
end
# If the condition satisfies, return the given A in left, otherwise, return the given B in right.
#
# @param [Object] lvalue The left value to use if the condition satisfies.
# @param [Object] rvalue The right value to use if the condition does not satisfy.
# @param [Boolean] condition The condition to test (when no block given).
# @yield The condition to test (when no condition given).
#
# @return [Either] A constructed either based on the given condition.
#
# @raise [ArgumentError] When both a condition and a block are given.
def self.iff(lvalue, rvalue, condition = NO_VALUE)
raise ArgumentError.new('requires either a condition or a block, not both') if condition != NO_VALUE && block_given?
condition = block_given? ? yield : !! condition
condition ? left(lvalue) : right(rvalue)
end
private
# Create a new Either wil the given value and disposition.
#
# @param [Object] value the value of this either
# @param [Boolean] is_left is this a left either or right?
#
# @!visibility private
def initialize(value, is_left)
super
@is_left = is_left
hsh = is_left ? {left: value, right: nil} : {left: nil, right: value}
set_data_hash(hsh)
set_values_array(hsh.values)
ensure_ivar_visibility!
end
end
end
================================================
FILE: lib/functional/final_struct.rb
================================================
require 'functional/final_var'
require 'functional/synchronization'
module Functional
# A variation on Ruby's `OpenStruct` in which all fields are "final" (meaning
# that new fields can be arbitrarily added to a `FinalStruct` object but once
# set each field becomes immutable). Additionally, predicate methods exist for
# all fields and these predicates indicate if the field has been set.
#
# There are two ways to initialize a `FinalStruct`: with zero arguments or
# with a `Hash` (or any other object that implements a `to_h` method). The
# only difference in behavior is that a `FinalStruct` initialized with a
# hash will pre-define and pre-populate attributes named for the hash keys
# and with values corresponding to the hash values.
#
# @example Instanciation With No Fields
# bucket = Functional::FinalStruct.new
#
# bucket.foo #=> nil
# bucket.foo? #=> false
#
# bucket.foo = 42 #=> 42
# bucket.foo #=> 42
# bucket.foo? #=> true
#
# bucket.foo = 42 #=> Functional::FinalityError: final accessor 'bar' has already been set
#
# @example Instanciation With a Hash
# name = Functional::FinalStruct.new(first: 'Douglas', last: 'Adams')
#
# name.first #=> 'Douglas'
# name.last #=> 'Adams'
# name.first? #=> true
# name.last? #=> true
#
# name.middle #=> nil
# name.middle? #=> false
# name.middle = 'Noel' #=> 'Noel'
# name.middle? #=> true
#
# name.first = 'Sam' #=> Functional::FinalityError: final accessor 'first' has already been set
#
# @see http://www.ruby-doc.org/stdlib-2.1.2/libdoc/ostruct/rdoc/OpenStruct.html
# @see http://en.wikipedia.org/wiki/Final_(Java) Java `final` keyword
#
# @!macro thread_safe_final_object
class FinalStruct < Synchronization::Object
# Creates a new `FinalStruct` object. By default, the resulting `FinalStruct`
# object will have no attributes. The optional hash, if given, will generate
# attributes and values (can be a `Hash` or any object with a `to_h` method).
#
# @param [Hash] attributes the field/value pairs to set on creation
def initialize(attributes = {})
raise ArgumentError.new('attributes must be given as a hash or not at all') unless attributes.respond_to?(:to_h)
super
synchronize do
@attribute_hash = {}
attributes.to_h.each_pair do |field, value|
ns_set_attribute(field, value)
end
end
end
# @!macro [attach] final_struct_get_method
#
# Get the value of the given field.
#
# @param [Symbol] field the field to retrieve the value for
# @return [Object] the value of the field is set else nil
def get(field)
synchronize { ns_get_attribute(field) }
end
alias_method :[], :get
# @!macro [attach] final_struct_set_method
#
# Set the value of the give field to the given value.
#
# It is a logical error to attempt to set a `final` field more than once, as this
# violates the concept of finality. Calling the method a second or subsequent time
# for a given field will result in an exception being raised.
#
# @param [Symbol] field the field to set the value for
# @param [Object] value the value to set the field to
# @return [Object] the final value of the given field
#
# @raise [Functional::FinalityError] if the given field has already been set
def set(field, value)
synchronize do
if ns_attribute_has_been_set?(field)
raise FinalityError.new("final accessor '#{field}' has already been set")
else
ns_set_attribute(field, value)
end
end
end
alias_method :[]=, :set
# @!macro [attach] final_struct_set_predicate
#
# Check the internal hash to unambiguously verify that the given
# attribute has been set.
#
# @param [Symbol] field the field to get the value for
# @return [Boolean] true if the field has been set else false
def set?(field)
synchronize { ns_attribute_has_been_set?(field) }
end
# Get the current value of the given field if already set else set the value of
# the given field to the given value.
#
# @param [Symbol] field the field to get or set the value for
# @param [Object] value the value to set the field to when not previously set
# @return [Object] the final value of the given field
def get_or_set(field, value)
synchronize { ns_attribute_has_been_set?(field) ? ns_get_attribute(field) : ns_set_attribute(field, value) }
end
# Get the current value of the given field if already set else return the given
# default value.
#
# @param [Symbol] field the field to get the value for
# @param [Object] default the value to return if the field has not been set
# @return [Object] the value of the given field else the given default value
def fetch(field, default)
synchronize { ns_attribute_has_been_set?(field) ? ns_get_attribute(field) : default }
end
# Calls the block once for each attribute, passing the key/value pair as parameters.
# If no block is given, an enumerator is returned instead.
#
# @yieldparam [Symbol] field the struct field for the current iteration
# @yieldparam [Object] value the value of the current field
#
# @return [Enumerable] when no block is given
def each_pair
return enum_for(:each_pair) unless block_given?
synchronize do
@attribute_hash.each do |field, value|
yield(field, value)
end
end
end
# Converts the `FinalStruct` to a `Hash` with keys representing each attribute
# (as symbols) and their corresponding values.
#
# @return [Hash] a `Hash` representing this struct
def to_h
synchronize { @attribute_hash.dup }
end
# Compares this object and other for equality. A `FinalStruct` is `eql?` to
# other when other is a `FinalStruct` and the two objects have identical
# fields and values.
#
# @param [Object] other the other record to compare for equality
# @return [Boolean] true when equal else false
def eql?(other)
other.is_a?(self.class) && to_h == other.to_h
end
alias_method :==, :eql?
# Describe the contents of this object in a string.
#
# @return [String] the string representation of this object
#
# @!visibility private
def inspect
state = to_h.to_s.gsub(/^{/, '').gsub(/}$/, '')
"#<#{self.class} #{state}>"
end
alias_method :to_s, :inspect
protected
# @!macro final_struct_get_method
# @!visibility private
def ns_get_attribute(field)
@attribute_hash[field.to_sym]
end
# @!macro final_struct_set_method
# @!visibility private
def ns_set_attribute(field, value)
@attribute_hash[field.to_sym] = value
end
# @!macro final_struct_set_predicate
# @!visibility private
def ns_attribute_has_been_set?(field)
@attribute_hash.has_key?(field.to_sym)
end
# Check the method name and args for signatures matching potential
# final attribute reader, writer, and predicate methods. If the signature
# matches a reader or predicate, treat the attribute as unset. If the
# signature matches a writer, attempt to set the new attribute.
#
# @param [Symbol] symbol the name of the called function
# @param [Array] args zero or more arguments
# @return [Object] the result of the proxied method or the `super` call
#
# @!visibility private
def method_missing(symbol, *args)
if args.length == 1 && (match = /([^=]+)=$/.match(symbol))
set(match[1], args.first)
elsif args.length == 0 && (match = /([^\?]+)\?$/.match(symbol))
set?(match[1])
elsif args.length == 0
get(symbol)
else
super
end
end
end
end
================================================
FILE: lib/functional/final_var.rb
================================================
require 'functional/synchronization'
module Functional
# An exception raised when an attempt is made to modify an
# immutable object or attribute.
FinalityError = Class.new(StandardError)
# A thread safe object that holds a single value and is "final" (meaning
# that the value can be set at most once after which it becomes immutable).
# The value can be set at instantiation which will result in the object
# becoming fully and immediately immutable. Attempting to set the value
# once it has been set is a logical error and will result in an exception
# being raised.
#
# @example Instanciation With No Value
# f = Functional::FinalVar.new
# #=> #<Functional::FinalVar unset>
# f.set? #=> false
# f.value #=> nil
# f.value = 42 #=> 42
# f.inspect
# #=> "#<Functional::FinalVar value=42>"
# f.set? #=> true
# f.value #=> 42
#
# @example Instanciation With an Initial Value
# f = Functional::FinalVar.new(42)
# #=> #<Functional::FinalVar value=42>
# f.set? #=> true
# f.value #=> 42
#
# @see Functional::FinalStruct
# @see http://en.wikipedia.org/wiki/Final_(Java) Java `final` keyword
#
# @!macro [new] thread_safe_final_object
#
# @note This is a write-once, read-many, thread safe object that can
# be used in concurrent systems. Thread safety guarantees *cannot* be made
# about objects contained *within* this object, however. Ruby variables are
# mutable references to mutable objects. This cannot be changed. The best
# practice it to only encapsulate immutable, frozen, or thread safe objects.
# Ultimately, thread safety is the responsibility of the programmer.
class FinalVar < Synchronization::Object
# @!visibility private
NO_VALUE = Object.new.freeze
# Create a new `FinalVar` with the given value or "unset" when
# no value is given.
#
# @param [Object] value if given, the immutable value of the object
def initialize(value = NO_VALUE)
super
synchronize{ @value = value }
end
# Get the current value or nil if unset.
#
# @return [Object] the current value or nil
def get
synchronize { has_been_set? ? @value : nil }
end
alias_method :value, :get
# Set the value. Will raise an exception if already set.
#
# @param [Object] value the value to set
# @return [Object] the new value
# @raise [Functional::FinalityError] if the value has already been set
def set(value)
synchronize do
if has_been_set?
raise FinalityError.new('value has already been set')
else
@value = value
end
end
end
alias_method :value=, :set
# Has the value been set?
#
# @return [Boolean] true when the value has been set else false
def set?
synchronize { has_been_set? }
end
alias_method :value?, :set?
# Get the value if it has been set else set the value.
#
# @param [Object] value the value to set
# @return [Object] the current value if already set else the new value
def get_or_set(value)
synchronize do
if has_been_set?
@value
else
@value = value
end
end
end
# Get the value if set else return the given default value.
#
# @param [Object] default the value to return if currently unset
# @return [Object] the current value when set else the given default
def fetch(default)
synchronize { has_been_set? ? @value : default }
end
# Compares this object and other for equality. A `FinalVar` that is unset
# is never equal to anything else (it represents a complete absence of value).
# When set a `FinalVar` is equal to another `FinalVar` if they have the same
# value. A `FinalVar` is equal to another object if its value is equal to
# the other object using Ruby's normal equality rules.
#
# @param [Object] other the object to compare equality to
# @return [Boolean] true if equal else false
def eql?(other)
if (val = fetch(NO_VALUE)) == NO_VALUE
false
elsif other.is_a?(FinalVar)
val == other.value
else
val == other
end
end
alias_method :==, :eql?
# Describe the contents of this object in a string.
#
# @return [String] the string representation of this object
#
# @!visibility private
def inspect
if (val = fetch(NO_VALUE)) == NO_VALUE
val = 'unset'
else
val = "value=#{val.is_a?(String) ? ('"' + val + '"') : val }"
end
"#<#{self.class} #{val}>"
end
# Describe the contents of this object in a string.
#
# @return [String] the string representation of this object
#
# @!visibility private
def to_s
value.to_s
end
private
# Checks the set status without locking the mutex.
# @return [Boolean] true when set else false
def has_been_set?
@value != NO_VALUE
end
end
end
================================================
FILE: lib/functional/memo.rb
================================================
require 'functional/synchronization'
module Functional
# Memoization is a technique for optimizing functions that are time-consuming
# and/or involve expensive calculations. Every time a memoized function is
# called the result is caches with reference to the given parameters.
# Subsequent calls to the function that use the same parameters will return
# the cached result. As a result the response time for frequently called
# functions is vastly increased (after the first call with any given set of)
# arguments, at the cost of increased memory usage (the cache).
#
# {include:file:doc/memo.md}
#
# @note Memoized method calls are thread safe and can safely be used in
# concurrent systems. Declaring memoization on a function is *not* thread
# safe and should only be done during application initialization.
module Memo
# @!visibility private
def self.extended(base)
base.extend(ClassMethods)
base.send(:__method_memos__=, {})
super(base)
end
# @!visibility private
def self.included(base)
base.extend(ClassMethods)
base.send(:__method_memos__=, {})
super(base)
end
# @!visibility private
module ClassMethods
# @!visibility private
class Memoizer < Synchronization::Object
attr_reader :function, :cache, :max_cache
def initialize(function, max_cache)
super
synchronize do
@function = function
@max_cache = max_cache
@cache = {}
end
end
def max_cache?
max_cache > 0 && cache.size >= max_cache
end
public :synchronize
end
private_constant :Memoizer
# @!visibility private
attr_accessor :__method_memos__
# Returns a memoized version of a referentially transparent function. The
# memoized version of the function keeps a cache of the mapping from
# arguments to results and, when calls with the same arguments are
# repeated often, has higher performance at the expense of higher memory
# use.
#
# @param [Symbol] func the class/module function to memoize
# @param [Hash] opts the options controlling memoization
# @option opts [Fixnum] :at_most the maximum number of memos to store in
# the cache; a value of zero (the default) or `nil` indicates no limit
#
# @raise [ArgumentError] when the method has already been memoized
# @raise [ArgumentError] when :at_most option is a negative number
def memoize(func, opts = {})
func = func.to_sym
max_cache = opts[:at_most].to_i
raise ArgumentError.new("method :#{func} has already been memoized") if __method_memos__.has_key?(func)
raise ArgumentError.new(':max_cache must be > 0') if max_cache < 0
__method_memos__[func] = Memoizer.new(method(func), max_cache.to_i)
__define_memo_proxy__(func)
nil
end
# @!visibility private
def __define_memo_proxy__(func)
self.class_eval <<-RUBY
def self.#{func}(*args, &block)
self.__proxy_memoized_method__(:#{func}, *args, &block)
end
RUBY
end
# @!visibility private
def __proxy_memoized_method__(func, *args, &block)
memo = self.__method_memos__[func]
memo.synchronize do
if block_given?
memo.function.call(*args, &block)
elsif memo.cache.has_key?(args)
memo.cache[args]
else
result = memo.function.call(*args)
memo.cache[args] = result unless memo.max_cache?
end
end
end
end
end
end
================================================
FILE: lib/functional/method_signature.rb
================================================
module Functional
module PatternMatching
# @!visibility private
#
# Helper functions used when pattern matching runtime arguments against
# a method defined with the `defn` function of Functional::PatternMatching.
module MethodSignature
extend self
# Do the given arguments match the given function pattern?
#
# @return [Boolean] true when there is a match else false
def match?(pattern, args)
return false unless valid_pattern?(args, pattern)
pattern.length.times.all? do |index|
param = pattern[index]
arg = args[index]
all_param_and_last_arg?(pattern, param, index) ||
arg_is_type_of_param?(param, arg) ||
hash_param_with_matching_arg?(param, arg) ||
param_matches_arg?(param, arg)
end
end
# Is the given pattern a valid pattern with respect to the given
# runtime arguments?
#
# @return [Boolean] true when the pattern is valid else false
def valid_pattern?(args, pattern)
(pattern.last == PatternMatching::ALL && args.length >= pattern.length) \
|| (args.length == pattern.length)
end
# Is this the last parameter and is it `ALL`?
#
# @return [Boolean] true when matching else false
def all_param_and_last_arg?(pattern, param, index)
param == PatternMatching::ALL && index+1 == pattern.length
end
# Is the parameter a class and is the provided argument an instance
# of that class?
#
# @return [Boolean] true when matching else false
def arg_is_type_of_param?(param, arg)
param.is_a?(Class) && arg.is_a?(param)
end
# Is the given parameter a Hash and does it match the given
# runtime argument?
#
# @return [Boolean] true when matching else false
def hash_param_with_matching_arg?(param, arg)
param.is_a?(Hash) && arg.is_a?(Hash) && ! param.empty? && param.all? do |key, value|
arg.has_key?(key) && (value == PatternMatching::UNBOUND || arg[key] == value)
end
end
# Does the given parameter exactly match the given runtime
# argument or is the parameter `UNBOUND`?
#
# @return [Boolean] true when matching else false
def param_matches_arg?(param, arg)
param == PatternMatching::UNBOUND || param == arg
end
end
private_constant :MethodSignature
end
end
================================================
FILE: lib/functional/option.rb
================================================
require 'functional/abstract_struct'
require 'functional/either'
require 'functional/protocol'
require 'functional/synchronization'
Functional::SpecifyProtocol(:Option) do
instance_method :some?, 0
instance_method :none?, 0
instance_method :some, 0
end
module Functional
# An optional value that may be none (no value) or some (a value).
# This type is a replacement for the use of nil with better type checks.
# It is an immutable data structure that extends `AbstractStruct`.
#
# @see http://functionaljava.googlecode.com/svn/artifacts/3.0/javadoc/index.html Functional Java
#
# @!macro thread_safe_immutable_object
class Option < Synchronization::Object
include AbstractStruct
# @!visibility private
NO_OPTION = Object.new.freeze
self.datatype = :option
self.fields = [:some].freeze
private_class_method :new
# The reason for the absence of a value when none,
# defaults to nil
attr_reader :reason
class << self
# Construct an `Option` with no value.
#
# @return [Option] the new option
def none(reason = nil)
new(nil, true, reason).freeze
end
# Construct an `Option` with the given value.
#
# @param [Object] value the value of the option
# @return [Option] the new option
def some(value)
new(value, false).freeze
end
end
# Does the option have a value?
#
# @return [Boolean] true if some else false
def some?
! none?
end
alias_method :value?, :some?
alias_method :fulfilled?, :some?
# Is the option absent a value?
#
# @return [Boolean] true if none else false
def none?
@none
end
alias_method :reason?, :none?
alias_method :rejected?, :none?
# The value of this option.
#
# @return [Object] the value when some else nil
def some
to_h[:some]
end
alias_method :value, :some
# Returns the length of this optional value;
# 1 if there is a value, 0 otherwise.
#
# @return [Fixnum] The length of this optional value;
# 1 if there is a value, 0 otherwise.
def length
none? ? 0 : 1
end
alias_method :size, :length
# Perform a logical `and` operation against this option and the
# provided option or block. Returns true if this option is some and:
#
# * other is an `Option` with some value
# * other is a truthy value (not nil or false)
# * the result of the block is a truthy value
#
# If a block is given the value of the current option is passed to the
# block and the result of block processing will be evaluated for its
# truthiness. An exception will be raised if an other value and a
# block are both provided.
#
# @param [Object] other the value to be evaluated against this option
# @yieldparam [Object] value the value of this option when some
# @return [Boolean] true when the union succeeds else false
# @raise [ArgumentError] when given both other and a block
def and(other = NO_OPTION)
raise ArgumentError.new('cannot give both an option and a block') if other != NO_OPTION && block_given?
return false if none?
if block_given?
!! yield(some)
elsif Protocol::Satisfy? other, :Option
other.some?
else
!! other
end
end
# Perform a logical `or` operation against this option and the
# provided option or block. Returns true if this option is some.
# If this option is none it returns true if:
#
# * other is an `Option` with some value
# * other is a truthy value (not nil or false)
# * the result of the block is a truthy value
#
# If a block is given the value of the result of block processing
# will be evaluated for its truthiness. An exception will be raised
# if an other value and a block are both provided.
#
# @param [Object] other the value to be evaluated against this option
# @return [Boolean] true when the intersection succeeds else false
# @raise [ArgumentError] when given both other and a block
def or(other = NO_OPTION)
raise ArgumentError.new('cannot give both an option and a block') if other != NO_OPTION && block_given?
return true if some?
if block_given?
!! yield
elsif Protocol::Satisfy? other, :Option
other.some?
else
!! other
end
end
# Returns the value of this option when some else returns the
# value of the other option or block. When the other is also an
# option its some value is returned. When the other is any other
# value it is simply passed through. When a block is provided the
# block is processed and the return value of the block is returned.
# An exception will be raised if an other value and a block are
# both provided.
#
# @param [Object] other the value to be evaluated when this is none
# @return [Object] this value when some else the value of other
# @raise [ArgumentError] when given both other and a block
def else(other = NO_OPTION)
raise ArgumentError.new('cannot give both an option and a block') if other != NO_OPTION && block_given?
return some if some?
if block_given?
yield
elsif Protocol::Satisfy? other, :Option
other.some
else
other
end
end
# If the condition satisfies, return the given A in some, otherwise, none.
#
# @param [Object] value The some value to use if the condition satisfies.
# @param [Boolean] condition The condition to test (when no block given).
# @yield The condition to test (when no condition given).
#
# @return [Option] A constructed option based on the given condition.
#
# @raise [ArgumentError] When both a condition and a block are given.
def self.iff(value, condition = NO_OPTION)
raise ArgumentError.new('requires either a condition or a block, not both') if condition != NO_OPTION && block_given?
condition = block_given? ? yield : !! condition
condition ? some(value) : none
end
# @!macro inspect_method
def inspect
super.gsub(/ :some/, " (#{some? ? 'some' : 'none'}) :some")
end
alias_method :to_s, :inspect
private
# Create a new Option with the given value and disposition.
#
# @param [Object] value the value of this option
# @param [Boolean] none is this option absent a value?
# @param [Object] reason the reason for the absense of a value
#
# @!visibility private
def initialize(value, none, reason = nil)
super
@none = none
@reason = none ? reason : nil
hsh = none ? {some: nil} : {some: value}
set_data_hash(hsh)
set_values_array(hsh.values)
ensure_ivar_visibility!
end
end
end
================================================
FILE: lib/functional/pattern_matching.rb
================================================
require 'functional/method_signature'
module Functional
# As much as I love Ruby I've always been a little disappointed that Ruby
# doesn't support function overloading. Function overloading tends to reduce
# branching and keep function signatures simpler. No sweat, I learned to do
# without. Then I started programming in Erlang. My favorite Erlang feature
# is, without question, pattern matching. Pattern matching is like function
# overloading cranked to 11. So one day I was musing on Twitter that I'd like
# to see Erlang-stype pattern matching in Ruby and one of my friends responded
# "Build it!" So I did. And here it is.
#
# {include:file:doc/pattern_matching.md}
module PatternMatching
# A parameter that is required but that can take any value.
# @!visibility private
UNBOUND = Object.new.freeze
# A match for one or more parameters in the last position of the match.
# @!visibility private
ALL = Object.new.freeze
private
# A guard clause on a pattern match.
# @!visibility private
GuardClause = Class.new do
def initialize(function, clazz, pattern)
@function = function
@clazz = clazz
@pattern = pattern
end
def when(&block)
unless block_given?
raise ArgumentError.new("block missing for `when` guard on function `#{@function}` of class #{@clazz}")
end
@pattern.guard = block
self
end
end
private_constant :GuardClause
# @!visibility private
FunctionPattern = Struct.new(:function, :args, :body, :guard)
private_constant :FunctionPattern
# @!visibility private
def __unbound_args__(match, args)
argv = []
match.args.each_with_index do |p, i|
if p == ALL && i == match.args.length-1
# when got ALL, then push all to the end to the list of args,
# so we can get them as usual *args in matched method
argv.concat args[(i..args.length)]
elsif p.is_a?(Hash) && p.values.include?(UNBOUND)
p.each do |key, value|
argv << args[i][key] if value == UNBOUND
end
elsif p.is_a?(Hash) || p == UNBOUND || p.is_a?(Class)
argv << args[i]
end
end
argv
end
def __pass_guard__?(matcher, args)
matcher.guard.nil? ||
self.instance_exec(*__unbound_args__(matcher, args), &matcher.guard)
end
# @!visibility private
def __pattern_match__(clazz, function, *args, &block)
args = args.first
matchers = clazz.__function_pattern_matches__.fetch(function, [])
matchers.detect do |matcher|
MethodSignature.match?(matcher.args, args) && __pass_guard__?(matcher, args)
end
end
# @!visibility private
def self.included(base)
base.extend(ClassMethods)
super(base)
end
# Class methods added to a class that includes {Functional::PatternMatching}
# @!visibility private
module ClassMethods
# @!visibility private
def _()
UNBOUND
end
# @!visibility private
def defn(function, *args, &block)
unless block_given?
raise ArgumentError.new("block missing for definition of function `#{function}` on class #{self}")
end
# Check that number of free variables in pattern match method's arity
pat_arity = __pattern_arity__(args)
unless pat_arity == block.arity
raise ArgumentError.new("Pattern and block arity mismatch: "\
"#{pat_arity}, #{block.arity}")
end
# add a new pattern for this function
pattern = __register_pattern__(function, *args, &block)
# define the delegator function if it doesn't exist yet
unless self.instance_methods(false).include?(function)
__define_method_with_matching__(function)
end
# return a guard clause to be added to the pattern
GuardClause.new(function, self, pattern)
end
# @!visibility private
# define an arity -1 function that dispatches to the appropriate
# pattern match variant or raises an exception
def __define_method_with_matching__(function)
define_method(function) do |*args, &block|
begin
# get the collection of matched patterns for this function
# use owner to ensure we climb the inheritance tree
match = __pattern_match__(self.method(function).owner, function, args, block)
if match
# call the matched function
argv = __unbound_args__(match, args)
self.instance_exec(*argv, &match.body)
elsif defined?(super)
# delegate to the superclass
super(*args, &block)
else
raise NoMethodError.new("no method `#{function}` matching "\
"#{args} found for class #{self.class}")
end
end
end
end
# @!visibility private
def __function_pattern_matches__
@__function_pattern_matches__ ||= Hash.new
end
# @!visibility private
def __register_pattern__(function, *args, &block)
block = Proc.new{} unless block_given?
pattern = FunctionPattern.new(function, args, block)
patterns = self.__function_pattern_matches__.fetch(function, [])
patterns << pattern
self.__function_pattern_matches__[function] = patterns
pattern
end
# @!visibility private
def __pattern_arity__(pat)
r = pat.reduce(0) do |acc, v|
if v.is_a?(Hash)
ub = v.values.count { |e| e == UNBOUND }
# if hash have UNBOUND then treat each unbound as separate arg
# alse all hash is one arg
ub > 0 ? acc + ub : acc + 1
elsif v == ALL || v == UNBOUND || v.is_a?(Class)
acc + 1
else
acc
end
end
pat.last == ALL ? -r : r
end
end
end
end
================================================
FILE: lib/functional/protocol.rb
================================================
require 'functional/protocol_info'
module Functional
# An exception indicating a problem during protocol processing.
ProtocolError = Class.new(StandardError)
# Specify a new protocol or retrieve the specification of an existing
# protocol.
#
# When called without a block the global protocol registry will be searched
# for a protocol with the matching name. If found the corresponding
# {Functional::ProtocolInfo} object will be returned. If not found `nil` will
# be returned.
#
# When called with a block, a new protocol with the given name will be
# created and the block will be processed to provide the specifiction.
# When successful the new {Functional::ProtocolInfo} object will be returned.
# An exception will be raised if a protocol with the same name already
# exists.
#
# @example
# Functional::SpecifyProtocol(:Queue) do
# instance_method :push, 1
# instance_method :pop, 0
# instance_method :length, 0
# end
#
# @param [Symbol] name The global name of the new protocol
# @yield The protocol definition
# @return [Functional::ProtocolInfo] the newly created or already existing
# protocol specification
#
# @raise [Functional::ProtocolError] when attempting to specify a protocol
# that has already been specified.
#
# @see Functional::Protocol
def SpecifyProtocol(name, &block)
name = name.to_sym
protocol_info = Protocol.class_variable_get(:@@info)[name]
return protocol_info unless block_given?
if block_given? && protocol_info
raise ProtocolError.new(":#{name} has already been defined")
end
info = ProtocolInfo.new(name, &block)
Protocol.class_variable_get(:@@info)[name] = info
end
module_function :SpecifyProtocol
# Protocols provide a polymorphism and method-dispatch mechanism that eschews
# strong typing and embraces the dynamic duck typing of Ruby. Rather than
# interrogate a module, class, or object for its type and ancestry, protocols
# allow modules, classes, and methods to be interrogated based on their behavior.
# It is a logical extension of the `respond_to?` method, but vastly more powerful.
#
# {include:file:doc/protocol.md}
module Protocol
# The global registry of specified protocols.
@@info = {}
# Does the given module/class/object fully satisfy the given protocol(s)?
#
# @param [Object] target the method/class/object to interrogate
# @param [Symbol] protocols one or more protocols to check against the target
# @return [Boolean] true if the target satisfies all given protocols else false
#
# @raise [ArgumentError] when no protocols given
def Satisfy?(target, *protocols)
raise ArgumentError.new('no protocols given') if protocols.empty?
protocols.all?{|protocol| Protocol.satisfies?(target, protocol.to_sym) }
end
module_function :Satisfy?
# Does the given module/class/object fully satisfy the given protocol(s)?
# Raises a {Functional::ProtocolError} on failure.
#
# @param [Object] target the method/class/object to interrogate
# @param [Symbol] protocols one or more protocols to check against the target
# @return [Symbol] the target
#
# @raise [Functional::ProtocolError] when one or more protocols are not satisfied
# @raise [ArgumentError] when no protocols given
def Satisfy!(target, *protocols)
Protocol::Satisfy?(target, *protocols) or
Protocol.error(target, 'does not', *protocols)
target
end
module_function :Satisfy!
# Have the given protocols been specified?
#
# @param [Symbol] protocols the list of protocols to check
# @return [Boolean] true if all given protocols have been specified else false
#
# @raise [ArgumentError] when no protocols are given
def Specified?(*protocols)
raise ArgumentError.new('no protocols given') if protocols.empty?
Protocol.unspecified(*protocols).empty?
end
module_function :Specified?
# Have the given protocols been specified?
# Raises a {Functional::ProtocolError} on failure.
#
# @param [Symbol] protocols the list of protocols to check
# @return [Boolean] true if all given protocols have been specified
#
# @raise [Functional::ProtocolError] if one or more of the given protocols have
# not been specified
# @raise [ArgumentError] when no protocols are given
def Specified!(*protocols)
raise ArgumentError.new('no protocols given') if protocols.empty?
(unspecified = Protocol.unspecified(*protocols)).empty? or
raise ProtocolError.new("The following protocols are unspecified: :#{unspecified.join('; :')}.")
end
module_function :Specified!
private
# Does the target satisfy the given protocol?
#
# @param [Object] target the module/class/object to check
# @param [Symbol] protocol the protocol to check against the target
# @return [Boolean] true if the target satisfies the protocol else false
def self.satisfies?(target, protocol)
info = @@info[protocol]
return info && info.satisfies?(target)
end
# Reduces a list of protocols to a list of unspecified protocols.
#
# @param [Symbol] protocols the list of protocols to check
# @return [Array] zero or more unspecified protocols
def self.unspecified(*protocols)
protocols.drop_while do |protocol|
@@info.has_key? protocol.to_sym
end
end
# Raise a {Functional::ProtocolError} formatted with the given data.
#
# @param [Object] target the object that was being interrogated
# @param [String] message the message fragment to inject into the error
# @param [Symbol] protocols list of protocols that were being checked against the target
#
# @raise [Functional::ProtocolError] the formatted exception object
def self.error(target, message, *protocols)
target = target.class unless target.is_a?(Module)
raise ProtocolError,
"Value (#{target.class}) '#{target}' #{message} behave as all of: :#{protocols.join('; :')}."
end
end
end
================================================
FILE: lib/functional/protocol_info.rb
================================================
require 'functional/synchronization'
module Functional
# An immutable object describing a single protocol and capable of building
# itself from a block. Used by {Functional#SpecifyProtocol}.
#
# @see Functional::Protocol
class ProtocolInfo < Synchronization::Object
# The symbolic name of the protocol
attr_reader :name
# Process a protocol specification block and build a new object.
#
# @param [Symbol] name the symbolic name of the protocol
# @yield self to the given specification block
# @return [Functional::ProtocolInfo] the new info object, frozen
#
# @raise [ArgumentError] when name is nil or an empty string
# @raise [ArgumentError] when no block given
def initialize(name, &specification)
raise ArgumentError.new('no block given') unless block_given?
raise ArgumentError.new('no name given') if name.nil? || name.empty?
super
@name = name.to_sym
@info = Info.new({}, {}, [])
self.instance_eval(&specification)
@info.each_pair{|col, _| col.freeze}
@info.freeze
ensure_ivar_visibility!
self.freeze
end
# The instance methods expected by this protocol.
#
# @return [Hash] a frozen hash of all instance method names and their
# expected arity for this protocol
def instance_methods
@info.instance_methods
end
# The class methods expected by this protocol.
#
# @return [Hash] a frozen hash of all class method names and their
# expected arity for this protocol
def class_methods
@info.class_methods
end
# The constants expected by this protocol.
#
# @return [Array] a frozen list of the constants expected by this protocol
def constants
@info.constants
end
# Does the given module/class/object satisfy this protocol?
#
# @return [Boolean] true if the target satisfies this protocol else false
def satisfies?(target)
satisfies_constants?(target) &&
satisfies_instance_methods?(target) &&
satisfies_class_methods?(target)
end
private
# Data structure for encapsulating the protocol info data.
# @!visibility private
Info = Struct.new(:instance_methods, :class_methods, :constants)
# Does the target satisfy the constants expected by this protocol?
#
# @param [target] target the module/class/object to interrogate
# @return [Boolean] true when satisfied else false
def satisfies_constants?(target)
clazz = target.is_a?(Module) ? target : target.class
@info.constants.all?{|constant| clazz.const_defined?(constant) }
end
# Does the target satisfy the instance methods expected by this protocol?
#
# @param [target] target the module/class/object to interrogate
# @return [Boolean] true when satisfied else false
def satisfies_instance_methods?(target)
@info.instance_methods.all? do |method, arity|
if target.is_a? Module
target.method_defined?(method) && check_arity?(target.instance_method(method), arity)
else
target.respond_to?(method) && check_arity?(target.method(method), arity)
end
end
end
# Does the target satisfy the class methods expected by this protocol?
#
# @param [target] target the module/class/object to interrogate
# @return [Boolean] true when satisfied else false
def satisfies_class_methods?(target)
clazz = target.is_a?(Module) ? target : target.class
@info.class_methods.all? do |method, arity|
break false unless clazz.respond_to? method
method = clazz.method(method)
check_arity?(method, arity)
end
end
# Does the given method have the expected arity? Returns true
# if the arity of the method is `-1` (variable length argument list
# with no required arguments), when expected is `nil` (indicating any
# arity is acceptable), or the arity of the method exactly matches the
# expected arity.
#
# @param [Method] method the method object to interrogate
# @param [Fixnum] expected the expected arity
# @return [Boolean] true when an acceptable match else false
#
# @see http://www.ruby-doc.org/core-2.1.2/Method.html#method-i-arity Method#arity
def check_arity?(method, expected)
arity = method.arity
expected.nil? || arity == -1 || expected == arity
end
#################################################################
# DSL methods
# Specify an instance method.
#
# @param [Symbol] name the name of the method
# @param [Fixnum] arity the required arity
def instance_method(name, arity = nil)
arity = arity.to_i unless arity.nil?
@info.instance_methods[name.to_sym] = arity
end
# Specify a class method.
#
# @param [Symbol] name the name of the method
# @param [Fixnum] arity the required arity
def class_method(name, arity = nil)
arity = arity.to_i unless arity.nil?
@info.class_methods[name.to_sym] = arity
end
# Specify an instance reader attribute.
#
# @param [Symbol] name the name of the attribute
def attr_reader(name)
instance_method(name, 0)
end
# Specify an instance writer attribute.
#
# @param [Symbol] name the name of the attribute
def attr_writer(name)
instance_method("#{name}=".to_sym, 1)
end
# Specify an instance accessor attribute.
#
# @param [Symbol] name the name of the attribute
def attr_accessor(name)
attr_reader(name)
attr_writer(name)
end
# Specify a class reader attribute.
#
# @param [Symbol] name the name of the attribute
def class_attr_reader(name)
class_method(name, 0)
end
# Specify a class writer attribute.
#
# @param [Symbol] name the name of the attribute
def class_attr_writer(name)
class_method("#{name}=".to_sym, 1)
end
# Specify a class accessor attribute.
#
# @param [Symbol] name the name of the attribute
def class_attr_accessor(name)
class_attr_reader(name)
class_attr_writer(name)
end
# Specify a constant.
#
# @param [Symbol] name the name of the constant
def constant(name)
@info.constants << name.to_sym
end
end
end
================================================
FILE: lib/functional/record.rb
================================================
require 'functional/abstract_struct'
require 'functional/protocol'
require 'functional/type_check'
module Functional
# An immutable data structure with multiple data fields. A `Record` is a
# convenient way to bundle a number of field attributes together,
# using accessor methods, without having to write an explicit class.
# The `Record` module generates new `AbstractStruct` subclasses that hold a
# set of fields with a reader method for each field.
#
# A `Record` is very similar to a Ruby `Struct` and shares many of its behaviors
# and attributes. Unlike a # Ruby `Struct`, a `Record` is immutable: its values
# are set at construction and can never be changed. Divergence between the two
# classes derive from this core difference.
#
# {include:file:doc/record.md}
#
# @see Functional::Union
# @see Functional::Protocol
# @see Functional::TypeCheck
#
# @!macro thread_safe_immutable_object
module Record
extend self
# Create a new record class with the given fields.
#
# @return [Functional::AbstractStruct] the new record subclass
# @raise [ArgumentError] no fields specified or an invalid type
# specification is given
def new(*fields, &block)
raise ArgumentError.new('no fields provided') if fields.empty?
name = nil
types = nil
# check if a name for registration is given
if fields.first.is_a?(String)
name = fields.first
fields = fields[1..fields.length-1]
end
# check for a set of type/protocol specifications
if fields.size == 1 && fields.first.respond_to?(:to_h)
types = fields.first
fields = fields.first.keys
check_types!(types)
end
build(name, fields, types, &block)
rescue
raise ArgumentError.new('invalid specification')
end
private
# @!visibility private
#
# A set of restrictions governing the creation of a new record.
class Restrictions
include Protocol
include TypeCheck
# Create a new restrictions object by processing the given
# block. The block should be the DSL for defining a record class.
#
# @param [Hash] types a hash of fields and the associated type/protocol
# when type/protocol checking is among the restrictions
# @param [Proc] block A DSL definition of a new record.
# @yield A DSL definition of a new record.
def initialize(types = nil, &block)
@types = types
@required = []
@defaults = {}
instance_eval(&block) if block_given?
@required.freeze
@defaults.freeze
self.freeze
end
# DSL method for declaring one or more fields to be mandatory.
#
# @param [Symbol] fields zero or more mandatory fields
def mandatory(*fields)
@required.concat(fields.collect{|field| field.to_sym})
end
# DSL method for declaring a default value for a field
#
# @param [Symbol] field the field to be given a default value
# @param [Object] value the default value of the field
def default(field, value)
@defaults[field] = value
end
# Clone a default value if it is cloneable. Else just return
# the value.
#
# @param [Symbol] field the name of the field from which the
# default value is to be cloned.
# @return [Object] a clone of the value or the value if uncloneable
def clone_default(field)
value = @defaults[field]
value = value.clone unless uncloneable?(value)
rescue TypeError
# can't be cloned
ensure
return value
end
# Validate the record data against this set of restrictions.
#
# @param [Hash] data the data hash
# @raise [ArgumentError] when the data does not match the restrictions
def validate!(data)
validate_mandatory!(data)
validate_types!(data)
end
private
# Check the given data hash to see if it contains non-nil values for
# all mandatory fields.
#
# @param [Hash] data the data hash
# @raise [ArgumentError] if any mandatory fields are missing
def validate_mandatory!(data)
if data.any?{|k,v| @required.include?(k) && v.nil? }
raise ArgumentError.new('mandatory fields must not be nil')
end
end
# Validate the record data against a type/protocol specification.
#
# @param [Hash] data the data hash
# @raise [ArgumentError] when the data does not match the specification
def validate_types!(data)
return if @types.nil?
@types.each do |field, type|
value = data[field]
next if value.nil?
if type.is_a? Module
raise ArgumentError.new("'#{field}' must be of type #{type}") unless Type?(value, type)
else
raise ArgumentError.new("'#{field}' must stasify the protocol :#{type}") unless Satisfy?(value, type)
end
end
end
# Is the given object uncloneable?
#
# @param [Object] object the object to check
# @return [Boolean] true if the object cannot be cloned else false
def uncloneable?(object)
Type? object, NilClass, TrueClass, FalseClass, Fixnum, Bignum, Float
end
end
private_constant :Restrictions
# Validate the given type/protocol specification.
#
# @param [Hash] types the type specification
# @raise [ArgumentError] when the specification is not valid
def check_types!(types)
return if types.nil?
unless types.all?{|k,v| v.is_a?(Module) || v.is_a?(Symbol) }
raise ArgumentError.new('invalid specification')
end
end
# Use the given `AbstractStruct` class and build the methods necessary
# to support the given data fields.
#
# @param [String] name the name under which to register the record when given
# @param [Array] fields the list of symbolic names for all data fields
# @return [Functional::AbstractStruct] the record class
def build(name, fields, types, &block)
fields = [name].concat(fields) unless name.nil?
record, fields = AbstractStruct.define_class(self, :record, fields)
record.class_variable_set(:@@restrictions, Restrictions.new(types, &block))
define_initializer(record)
fields.each do |field|
define_reader(record, field)
end
record
end
# Define an initializer method on the given record class.
#
# @param [Functional::AbstractStruct] record the new record class
# @return [Functional::AbstractStruct] the record class
def define_initializer(record)
record.send(:define_method, :initialize) do |data = {}|
super()
restrictions = record.class_variable_get(:@@restrictions)
data = record.fields.reduce({}) do |memo, field|
memo[field] = data.fetch(field, restrictions.clone_default(field))
memo
end
restrictions.validate!(data)
set_data_hash(data)
set_values_array(data.values)
ensure_ivar_visibility!
self.freeze
end
record
end
# Define a reader method on the given record class for the given data field.
#
# @param [Functional::AbstractStruct] record the new record class
# @param [Symbol] field symbolic name of the current data field
# @return [Functional::AbstractStruct] the record class
def define_reader(record, field)
record.send(:define_method, field) do
to_h[field]
end
record
end
end
end
================================================
FILE: lib/functional/synchronization.rb
================================================
module Functional
# @!visibility private
#
# Based on work originally done by Petr Chalupa (@pitr-ch) in Concurrent Ruby.
# https://github.com/ruby-concurrency/concurrent-ruby/blob/master/lib/concurrent/synchronization/object.rb
module Synchronization
if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
require 'jruby'
# @!visibility private
class Object
# @!visibility private
def initialize(*args)
end
protected
# @!visibility private
def synchronize
JRuby.reference0(self).synchronized { yield }
end
# @!visibility private
def ensure_ivar_visibility!
# relying on undocumented behavior of JRuby, ivar access is volatile
end
end
elsif defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx'
# @!visibility private
class Object
# @!visibility private
def initialize(*args)
end
protected
# @!visibility private
def synchronize(&block)
Rubinius.synchronize(self, &block)
end
# @!visibility private
def ensure_ivar_visibility!
# Rubinius instance variables are not volatile so we need to insert barrier
Rubinius.memory_barrier
end
end
else
require 'thread'
# @!visibility private
class Object
# @!visibility private
def initialize(*args)
@__lock__ = ::Mutex.new
@__condition__ = ::ConditionVariable.new
end
protected
# @!visibility private
def synchronize
if @__lock__.owned?
yield
else
@__lock__.synchronize { yield }
end
end
# @!visibility private
def ensure_ivar_visibility!
# relying on undocumented behavior of CRuby, GVL acquire has lock which ensures visibility of ivars
# https://github.com/ruby/ruby/blob/ruby_2_2/thread_pthread.c#L204-L211
end
end
end
end
end
================================================
FILE: lib/functional/tuple.rb
================================================
require 'functional/synchronization'
module Functional
# A tuple is a pure functional data strcture that is similar to an array but is
# immutable and of fixed length. Tuples support many of the same operations as
# array/list/vector.
#
# @note The current implementation uses simple Ruby arrays. This is likely to be
# very inefficient for all but the smallest tuples. The more items the tuple
# contains, the less efficient it will become. A future version will use a fast,
# immutable, persistent data structure such as a finger tree or a trie.
#
# @!macro thread_safe_immutable_object
#
# @see http://en.wikipedia.org/wiki/Tuple
# @see http://msdn.microsoft.com/en-us/library/system.tuple.aspx
# @see http://www.tutorialspoint.com/python/python_tuples.htm
# @see http://en.cppreference.com/w/cpp/utility/tuple
# @see http://docs.oracle.com/javaee/6/api/javax/persistence/Tuple.html
# @see http://www.erlang.org/doc/reference_manual/data_types.html
# @see http://www.erlang.org/doc/man/erlang.html#make_tuple-2
# @see http://en.wikibooks.org/wiki/Haskell/Lists_and_tuples#Tuples
class Tuple < Synchronization::Object
# Create a new tuple with the given data items in the given order.
#
# @param [Array] data the data items to insert into the new tuple
# @raise [ArgumentError] if data is not an array or does not implement `to_a`
def initialize(data = [])
raise ArgumentError.new('data is not an array') unless data.respond_to?(:to_a)
super
@data = data.to_a.dup.freeze
self.freeze
ensure_ivar_visibility!
end
# Retrieve the item at the given index. Indices begin at zero and increment
# up, just like Ruby arrays. Negative indicies begin at -1, which represents the
# last item in the tuple, and decrement toward the first item. If the
# given index is out of range then `nil` is returned.
#
# @param [Fixnum] index the index of the item to be retrieved
# @return [Object] the item at the given index or nil when index is out of bounds
def at(index)
@data[index]
end
alias_method :nth, :at
alias_method :[], :at
# Retrieve the item at the given index or return the given default value if the
# index is out of bounds. The behavior of indicies follows the rules for the
# `at` method.
#
# @param [Fixnum] index the index of the item to be retrieved
# @param [Object] default the value to return when given an out of bounds index
# @return [Object] the item at the given index or default when index is out of bounds
#
# @see Functional::Tuple#at
def fetch(index, default)
if index >= length || -index > length
default
else
at(index)
end
end
# The number of items in the tuple.
#
# @return [Fixnum] the number of items in the tuple
def length
@data.length
end
alias_method :size, :length
# Returns a new tuple containing elements common to the two tuples, excluding any
# duplicates. The order is preserved from the original tuple.
#
# @!macro [attach] tuple_method_param_other_return_tuple
# @param [Array] other the tuple or array-like object (responds to `to_a`) to operate on
# @return [Functional::Tuple] a new tuple with the appropriate items
def intersect(other)
Tuple.new(@data & other.to_a)
end
alias_method :&, :intersect
# Returns a new tuple by joining self with other, excluding any duplicates and
# preserving the order from the original tuple.
#
# @!macro tuple_method_param_other_return_tuple
def union(other)
Tuple.new(@data | other.to_a)
end
alias_method :|, :union
# Returns a new tuple built by concatenating the two tuples
# together to produce a third tuple.
#
# @!macro tuple_method_param_other_return_tuple
def concat(other)
Tuple.new(@data + other.to_a)
end
alias_method :+, :concat
# Returns a new tuple that is a copy of the original tuple, removing any items that
# also appear in other. The order is preserved from the original tuple.
#
# @!macro tuple_method_param_other_return_tuple
def diff(other)
Tuple.new(@data - other.to_a)
end
alias_method :-, :diff
# Returns a new tuple built by concatenating the given number of copies of self.
# Returns an empty tuple when the multiple is zero.
#
# @param [Fixnum] multiple the number of times to concatenate self
# @return [Functional::Tuple] a new tuple with the appropriate items
# @raise [ArgumentError] when multiple is a negative number
def repeat(multiple)
multiple = multiple.to_i
raise ArgumentError.new('negative argument') if multiple < 0
Tuple.new(@data * multiple)
end
alias_method :*, :repeat
# Returns a new tuple by removing duplicate values in self.
#
# @return [Functional::Tuple] the new tuple with only unique items
def uniq
Tuple.new(@data.uniq)
end
# Calls the given block once for each element in self, passing that element as a parameter.
# An Enumerator is returned if no block is given.
#
# @yieldparam [Object] item the current item
# @return [Enumerable] when no block is given
def each
return enum_for(:each) unless block_given?
@data.each do |item|
yield(item)
end
end
# Calls the given block once for each element in self, passing that element
# and the current index as parameters. An Enumerator is returned if no block is given.
#
# @yieldparam [Object] item the current item
# @yieldparam [Fixnum] index the index of the current item
# @return [Enumerable] when no block is given
def each_with_index
return enum_for(:each_with_index) unless block_given?
@data.each_with_index do |item, index|
yield(item, index)
end
end
# Calls the given block once for each element in self, passing that element
# and a tuple with all the remaining items in the tuple. When the last item
# is reached ab empty tuple is passed as the second parameter. This is the
# classic functional programming `head|tail` list processing idiom.
# An Enumerator is returned if no block is given.
#
# @yieldparam [Object] head the current item for this iteration
# @yieldparam [Tuple] tail the remaining items (tail) or an empty tuple when
# processing the last item
# @return [Enumerable] when no block is given
def sequence
return enum_for(:sequence) unless block_given?
@data.length.times do |index|
last = @data.length - 1
if index == last
yield(@data[index], Tuple.new)
else
yield(@data[index], Tuple.new(@data.slice(index+1..last)))
end
end
end
# Compares this object and other for equality. A tuple is `eql?` to
# other when other is a tuple or an array-like object (any object that
# responds to `to_a`) and the two objects have identical values in the
# same foxed order.
#
# @param [Object] other the other tuple to compare for equality
# @return [Boolean] true when equal else false
def eql?(other)
@data == other.to_a
end
alias_method :==, :eql?
# Returns true if self contains no items.
#
# @return [Boolean] true when empty else false
def empty?
@data.empty?
end
# Returns the first element of the tuple or nil when empty.
#
# @return [Object] the first element or nil
def first
@data.first
end
alias_method :head, :first
# Returns a tuple containing all the items in self after the first
# item. Returns an empty tuple when empty or there is only one item.
#
# @return [Functional::Tuple] the tail of the tuple
def rest
if @data.length <= 1
Tuple.new
else
Tuple.new(@data.slice(1..@data.length-1))
end
end
alias_method :tail, :rest
# Create a standard Ruby mutable array containing the tuple items
# in the same order.
#
# @return [Array] the new array created from the tuple
def to_a
@data.dup
end
alias_method :to_ary, :to_a
# Describe the contents of this object in a string.
#
# @return [String] the string representation of this object
#
# @!visibility private
def inspect
"#<#{self.class}: #{@data.to_s}>"
end
# Describe the contents of this object in a string that exactly
# matches the string that would be created from an identical array.
#
# @return [String] the string representation of this object
#
# @!visibility private
def to_s
@data.to_s
end
end
end
================================================
FILE: lib/functional/type_check.rb
================================================
module Functional
# Supplies type-checking helpers whenever included.
#
# @see http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Actor/TypeCheck.html TypeCheck in Concurrent Ruby
module TypeCheck
# Performs an `is_a?` check of the given value object against the
# given list of modules and/or classes.
#
# @param [Object] value the object to interrogate
# @param [Module] types zero or more modules and/or classes to check
# the value against
# @return [Boolean] true on success
def Type?(value, *types)
types.any? { |t| value.is_a? t }
end
module_function :Type?
# Performs an `is_a?` check of the given value object against the
# given list of modules and/or classes. Raises an exception on failure.
#
# @param [Object] value the object to interrogate
# @param [Module] types zero or more modules and/or classes to check
# the value against
# @return [Object] the value object
#
# @raise [Functional::TypeError] when the check fails
def Type!(value, *types)
Type?(value, *types) or
TypeCheck.error(value, 'is not', types)
value
end
module_function :Type!
# Is the given value object is an instance of or descendant of
# one of the classes/modules in the given list?
#
# Performs the check using the `===` operator.
#
# @param [Object] value the object to interrogate
# @param [Module] types zero or more modules and/or classes to check
# the value against
# @return [Boolean] true on success
def Match?(value, *types)
types.any? { |t| t === value }
end
module_function :Match?
# Is the given value object is an instance of or descendant of
# one of the classes/modules in the given list? Raises an exception
# on failure.
#
# Performs the check using the `===` operator.
#
# @param [Object] value the object to interrogate
# @param [Module] types zero or more modules and/or classes to check
# the value against
# @return [Object] the value object
#
# @raise [Functional::TypeError] when the check fails
def Match!(value, *types)
Match?(value, *types) or
TypeCheck.error(value, 'is not matching', types)
value
end
module_function :Match!
# Is the given class a subclass or exact match of one or more
# of the modules and/or classes in the given list?
#
# @param [Class] value the class to interrogate
# @param [Class] types zero or more classes to check the value against
# the value against
# @return [Boolean] true on success
def Child?(value, *types)
Type?(value, Class) &&
types.any? { |t| value <= t }
end
module_function :Child?
# Is the given class a subclass or exact match of one or more
# of the modules and/or classes in the given list?
#
# @param [Class] value the class to interrogate
# @param [Class] types zero or more classes to check the value against
# @return [Class] the value class
#
# @raise [Functional::TypeError] when the check fails
def Child!(value, *types)
Child?(value, *types) or
TypeCheck.error(value, 'is not child', types)
value
end
module_function :Child!
private
# Create a {Functional::TypeError} object from the given data.
#
# @param [Object] value the class/method that was being interrogated
# @param [String] message the message fragment to inject into the error
# @param [Object] types list of modules and/or classes that were being
# checked against the value object
#
# @raise [Functional::TypeError] the formatted exception object
def self.error(value, message, types)
raise TypeError,
"Value (#{value.class}) '#{value}' #{message} any of: #{types.join('; ')}."
end
end
end
================================================
FILE: lib/functional/union.rb
================================================
require 'functional/abstract_struct'
require 'functional/synchronization'
module Functional
# An immutable data structure with multiple fields, only one of which
# can be set at any given time. A `Union` is a convenient way to bundle a
# number of field attributes together, using accessor methods, without having
# to write an explicit class.
#
# The `Union` module generates new `AbstractStruct` subclasses that hold a set of
# fields with one and only one value associated with a single field. For each
# field a reader method is created along with a predicate and a factory. The
# predicate method indicates whether or not the give field is set. The reader
# method returns the value of that field or `nil` when not set. The factory
# creates a new union with the appropriate field set with the given value.
#
# A `Union` is very similar to a Ruby `Struct` and shares many of its behaviors
# and attributes. Where a `Struct` can have zero or more values, each of which is
# assiciated with a field, a `Union` can have one and only one value. Unlike a
# Ruby `Struct`, a `Union` is immutable: its value is set at construction and
# it can never be changed. Divergence between the two classes derive from these
# two core differences.
#
# @example Creating a New Class
#
# LeftRightCenter = Functional::Union.new(:left, :right, :center) #=> LeftRightCenter
# LeftRightCenter.ancestors #=> [LeftRightCenter, Functional::AbstractStruct... ]
# LeftRightCenter.fields #=> [:left, :right, :center]
#
# prize = LeftRightCenter.right('One million dollars!') #=> #<union LeftRightCenter... >
# prize.fields #=> [:left, :right, :center]
# prize.values #=> [nil, "One million dollars!", nil]
#
# prize.left? #=> false
# prize.right? #=> true
# prize.center? #=> false
#
# prize.left #=> nil
# prize.right #=> "One million dollars!"
# prize.center #=> nil
#
# @example Registering a New Class with Union
#
# Functional::Union.new('Suit', :clubs, :diamonds, :hearts, :spades)
# #=> Functional::Union::Suit
#
# Functional::Union::Suit.hearts('Queen')
# #=> #<union Functional::Union::Suit :clubs=>nil, :diamonds=>nil, :hearts=>"Queen", :spades=>nil>
#
# @see Functional::Union
# @see http://www.ruby-doc.org/core-2.1.2/Struct.html Ruby `Struct` class
# @see http://en.wikipedia.org/wiki/Union_type "Union type" on Wikipedia
#
# @!macro thread_safe_immutable_object
module Union
extend self
# Create a new union class with the given fields.
#
# @return [Functional::AbstractStruct] the new union subclass
# @raise [ArgumentError] no fields specified
def new(*fields)
raise ArgumentError.new('no fields provided') if fields.empty?
build(fields)
end
private
# Use the given `AbstractStruct` class and build the methods necessary
# to support the given data fields.
#
# @param [Array] fields the list of symbolic names for all data fields
# @return [Functional::AbstractStruct] the union class
def build(fields)
union, fields = AbstractStruct.define_class(self, :union, fields)
union.private_class_method(:new)
define_properties(union)
define_initializer(union)
fields.each do |field|
define_reader(union, field)
define_predicate(union, field)
define_factory(union, field)
end
union
end
# Define the `field` and `value` attribute readers on the given union class.
#
# @param [Functional::AbstractStruct] union the new union class
# @return [Functional::AbstractStruct] the union class
def define_properties(union)
union.send(:attr_reader, :field)
union.send(:attr_reader, :value)
union
end
# Define a predicate method on the given union class for the given data field.
#
# @param [Functional::AbstractStruct] union the new union class
# @param [Symbol] field symbolic name of the current data field
# @return [Functional::AbstractStruct] the union class
def define_predicate(union, field)
union.send(:define_method, "#{field}?".to_sym) do
@field == field
end
union
end
# Define a reader method on the given union class for the given data field.
#
# @param [Functional::AbstractStruct] union the new union class
# @param [Symbol] field symbolic name of the current data field
# @return [Functional::AbstractStruct] the union class
def define_reader(union, field)
union.send(:define_method, field) do
send("#{field}?".to_sym) ? @value : nil
end
union
end
# Define an initializer method on the given union class.
#
# @param [Functional::AbstractStruct] union the new union class
# @return [Functional::AbstractStruct] the union class
def define_initializer(union)
union.send(:define_method, :initialize) do |field, value|
super()
@field = field
@value = value
data = fields.reduce({}) do |memo, field|
memo[field] = ( field == @field ? @value : nil )
memo
end
set_data_hash(data)
set_values_array(data.values)
ensure_ivar_visibility!
self.freeze
end
union
end
# Define a factory method on the given union class for the given data field.
#
# @param [Functional::AbstractStruct] union the new union class
# @param [Symbol] field symbolic name of the current data field
# @return [Functional::AbstractStruct] the union class
def define_factory(union, field)
union.class.send(:define_method, field) do |value|
new(field, value).freeze
end
union
end
end
end
================================================
FILE: lib/functional/value_struct.rb
================================================
require 'functional/synchronization'
module Functional
# A variation on Ruby's `OpenStruct` in which all fields are immutable and
# set at instantiation. For compatibility with {Functional::FinalStruct},
# predicate methods exist for all potential fields and these predicates
# indicate if the field has been set. Calling a predicate method for a field
# that does not exist on the struct will return false.
#
# Unlike {Functional::Record}, which returns a new class which can be used to
# create immutable objects, `ValueStruct` creates simple immutable objects.
#
# @example Instanciation
# name = Functional::ValueStruct.new(first: 'Douglas', last: 'Adams')
#
# name.first #=> 'Douglas'
# name.last #=> 'Adams'
# name.first? #=> true
# name.last? #=> true
# name.middle? #=> false
#
# @see Functional::Record
# @see Functional::FinalStruct
# @see http://www.ruby-doc.org/stdlib-2.1.2/libdoc/ostruct/rdoc/OpenStruct.html
#
# @!macro thread_safe_immutable_object
class ValueStruct < Synchronization::Object
def initialize(attributes)
raise ArgumentError.new('attributes must be given as a hash') unless attributes.respond_to?(:each_pair)
super
@attribute_hash = {}
attributes.each_pair do |field, value|
set_attribute(field, value)
end
@attribute_hash.freeze
ensure_ivar_visibility!
self.freeze
end
# Get the value of the given field.
#
# @param [Symbol] field the field to retrieve the value for
# @return [Object] the value of the field is set else nil
def get(field)
@attribute_hash[field.to_sym]
end
alias_method :[], :get
# Check the internal hash to unambiguously verify that the given
# attribute has been set.
#
# @param [Symbol] field the field to get the value for
# @return [Boolean] true if the field has been set else false
def set?(field)
@attribute_hash.has_key?(field.to_sym)
end
# Get the current value of the given field if already set else return the given
# default value.
#
# @param [Symbol] field the field to get the value for
# @param [Object] default the value to return if the field has not been set
# @return [Object] the value of the given field else the given default value
def fetch(field, default)
@attribute_hash.fetch(field.to_sym, default)
end
# Calls the block once for each attribute, passing the key/value pair as parameters.
# If no block is given, an enumerator is returned instead.
#
# @yieldparam [Symbol] field the struct field for the current iteration
# @yieldparam [Object] value the value of the current field
#
# @return [Enumerable] when no block is given
def each_pair
return enum_for(:each_pair) unless block_given?
@attribute_hash.each do |field, value|
yield(field, value)
end
end
# Converts the `ValueStruct` to a `Hash` with keys representing each attribute
# (as symbols) and their corresponding values.
#
# @return [Hash] a `Hash` representing this struct
def to_h
@attribute_hash.dup # dup removes the frozen flag
end
# Compares this object and other for equality. A `ValueStruct` is `eql?` to
# other when other is a `ValueStruct` and the two objects have identical
# fields and values.
#
# @param [Object] other the other record to compare for equality
# @return [Boolean] true when equal else false
def eql?(other)
other.is_a?(self.class) && @attribute_hash == other.to_h
end
alias_method :==, :eql?
# Describe the contents of this object in a string.
#
# @return [String] the string representation of this object
#
# @!visibility private
def inspect
state = @attribute_hash.to_s.gsub(/^{/, '').gsub(/}$/, '')
"#<#{self.class} #{state}>"
end
alias_method :to_s, :inspect
protected
# Set the value of the give field to the given value.
#
# @param [Symbol] field the field to set the value for
# @param [Object] value the value to set the field to
# @return [Object] the final value of the given field
#
# @!visibility private
def set_attribute(field, value)
@attribute_hash[field.to_sym] = value
end
# Check the method name and args for signatures matching potential
# final predicate methods. If the signature matches call the appropriate
# method
#
# @param [Symbol] symbol the name of the called function
# @param [Array] args zero or more arguments
# @return [Object] the result of the proxied method or the `super` call
#
# @!visibility private
def method_missing(symbol, *args)
if args.length == 0 && (match = /([^\?]+)\?$/.match(symbol))
set?(match[1])
elsif args.length == 0 && set?(symbol)
get(symbol)
else
super
end
end
end
end
================================================
FILE: lib/functional/version.rb
================================================
module Functional
# The current gem version.
VERSION = '1.3.0'
end
================================================
FILE: lib/functional.rb
================================================
require 'functional/delay'
require 'functional/either'
require 'functional/final_struct'
require 'functional/final_var'
require 'functional/memo'
require 'functional/option'
require 'functional/pattern_matching'
require 'functional/protocol'
require 'functional/protocol_info'
require 'functional/record'
require 'functional/tuple'
require 'functional/type_check'
require 'functional/union'
require 'functional/value_struct'
require 'functional/version'
Functional::SpecifyProtocol(:Disposition) do
instance_method :value, 0
instance_method :value?, 0
instance_method :reason, 0
instance_method :reason?, 0
instance_method :fulfilled?, 0
instance_method :rejected?, 0
end
# Erlang, Clojure, and Go inspired functional programming tools to Ruby.
module Functional
# Infinity
Infinity = 1/0.0
# Not a number
NaN = 0/0.0
end
================================================
FILE: spec/.gitignore
================================================
================================================
FILE: spec/functional/abstract_struct_shared.rb
================================================
shared_examples :abstract_struct do
specify { Functional::Protocol::Satisfy! struct_class, :Struct }
let(:other_struct) do
Class.new do
include Functional::AbstractStruct
self.fields = [:foo, :bar, :baz].freeze
self.datatype = :other_struct
end
end
context 'field collection' do
it 'contains all possible fields' do
expected_fields.each do |field|
expect(struct_class.fields).to include(field)
end
end
it 'is frozen' do
expect(struct_class.fields).to be_frozen
end
it 'does not overwrite fields for other structs' do
expect(struct_class.fields).to_not eq other_struct.fields
end
it 'is the same when called on the class and on an object' do
expect(struct_class.fields).to eq struct_object.fields
end
end
context 'readers' do
specify '#values returns all values in an array' do
expect(struct_object.values).to eq expected_values
end
specify '#values is frozen' do
expect(struct_object.values).to be_frozen
end
specify 'exist for each field' do
expected_fields.each do |field|
expect(struct_object).to respond_to(field)
expect(struct_object.method(field).arity).to eq 0
end
end
specify 'return the appropriate value all fields' do
expected_fields.each_with_index do |field, i|
expect(struct_object.send(field)).to eq expected_values[i]
end
end
end
context 'enumeration' do
specify '#each_pair with a block iterates over all fields and values' do
fields = []
values = []
struct_object.each_pair do |field, value|
fields << field
values << value
end
expect(fields).to eq struct_object.fields
expect(values).to eq struct_object.values
end
specify '#each_pair without a block returns an Enumerable' do
expect(struct_object.each_pair).to be_a Enumerable
end
specify '#each with a block iterates over all values' do
values = []
struct_object.each do |value|
values << value
end
expect(values).to eq struct_object.values
end
specify '#each without a block returns an Enumerable' do
expect(struct_object.each).to be_a Enumerable
end
end
context 'reflection' do
specify 'always creates frozen objects' do
expect(struct_object).to be_frozen
end
specify 'asserts equality for two structs of the same class with equal values' do
other = struct_object.dup
expect(struct_object).to eq other
expect(struct_object).to eql other
end
specify 'rejects equality for two structs of different classes' do
other = Struct.new(*expected_fields).new(*expected_values)
expect(struct_object).to_not eq other
expect(struct_object).to_not eql other
end
specify 'rejects equality for two structs of the same class with different values' do
expect(struct_object).to_not eq other_object
expect(struct_object).to_not eql other_struct
end
specify '#to_h returns a Hash with all field/value pairs' do
hsh = struct_object.to_h
expect(hsh.keys).to eq struct_object.fields
expect(hsh.values).to eq struct_object.values
end
specify '#inspect result is enclosed in brackets' do
expect(struct_object.inspect).to match(/^#</)
expect(struct_object.inspect).to match(/>$/)
end
specify '#inspect result has lowercase class name as first element' do
struct = described_class.to_s.split('::').last.downcase
expect(struct_object.inspect).to match(/^#<#{struct} /)
end
specify '#inspect includes all field/value pairs' do
struct_object.fields.each_with_index do |field, i|
value_regex = "\"?#{struct_object.values[i]}\"?"
expect(struct_object.inspect).to match(/:#{field}=>#{value_regex}/)
end
end
specify '#inspect is aliased as #to_s' do
expect(struct_object.inspect).to eq struct_object.to_s
end
specify '#length returns the number of fields' do
expect(struct_object.length).to eq struct_class.fields.length
expect(struct_object.length).to eq expected_fields.length
end
specify 'aliases #length as #size' do
expect(struct_object.length).to eq struct_object.size
end
end
end
================================================
FILE: spec/functional/complex_pattern_matching_spec.rb
================================================
require 'ostruct'
class Bar
def greet
return 'Hello, World!'
end
end
class Foo < Bar
include Functional::PatternMatching
attr_accessor :name
defn(:initialize) { @name = 'baz' }
defn(:initialize, _) {|name| @name = name.to_s }
defn(:greet, _) do |name|
"Hello, #{name}!"
end
defn(:greet, :male, _) { |name|
"Hello, Mr. #{name}!"
}
defn(:greet, :female, _) { |name|
"Hello, Ms. #{name}!"
}
defn(:greet, nil, _) { |name|
"Goodbye, #{name}!"
}
defn(:greet, _, _) { |_, name|
"Hello, #{name}!"
}
defn(:hashable, _, {foo: :bar}, _) { |_, opts, _|
:foo_bar
}
defn(:hashable, _, {foo: _, bar: _}, _) { |_, f, b, _|
[f, b]
}
defn(:hashable, _, {foo: _}, _) { |_, f, _|
f
}
defn(:hashable, _, {}, _) { |_,_,_|
:empty
}
defn(:hashable, _, _, _) { |_, _, _|
:unbound
}
defn(:options, _) { |opts|
opts
}
defn(:recurse) {
'w00t!'
}
defn(:recurse, :match) {
recurse()
}
defn(:recurse, :super) {
greet()
}
defn(:recurse, :instance) {
@name
}
defn(:recurse, _) { |arg|
arg
}
defn(:concat, Integer, Integer) { |first, second|
first + second
}
defn(:concat, Integer, String) { |first, second|
"#{first} #{second}"
}
defn(:concat, String, String) { |first, second|
first + second
}
defn(:concat, Integer, UNBOUND) { |first, second|
first + second.to_i
}
defn(:all, :one, ALL) { |*args|
args
}
defn(:all, :one, Integer, ALL) { |int, *args|
[int, args]
}
defn(:all, 1, _, ALL) { |var, *args|
[var, args]
}
defn(:all, ALL) { |*args|
args
}
defn(:old_enough, _){ |_| true }.when{|x| x >= 16 }
defn(:old_enough, _){ |_| false }
defn(:right_age, _) { |_|
true
}.when{|x| x >= 16 && x <= 104 }
defn(:right_age, _) { |_|
false
}
defn(:wrong_age, _) { |_|
true
}.when{|x| x < 16 || x > 104 }
defn(:wrong_age, _) { |_|
false
}
end
class Baz < Foo
def boom_boom_room
'zoom zoom zoom'
end
def who(first, last)
[first, last].join(' ')
end
end
class Fizzbuzz < Baz
include Functional::PatternMatching
defn(:who, Integer) { |count|
(1..count).each.reduce(:+)
}
defn(:who) { 0 }
end
describe 'complex pattern matching' do
let(:name) { 'Pattern Matcher' }
subject { Foo.new(name) }
specify { expect(subject.greet).to eq 'Hello, World!' }
specify { expect(subject.greet('Jerry')).to eq 'Hello, Jerry!' }
specify { expect(subject.greet(:male, 'Jerry')).to eq 'Hello, Mr. Jerry!' }
specify { expect(subject.greet(:female, 'Jeri')).to eq 'Hello, Ms. Jeri!' }
specify { expect(subject.greet(:unknown, 'Jerry')).to eq 'Hello, Jerry!' }
specify { expect(subject.greet(nil, 'Jerry')).to eq 'Goodbye, Jerry!' }
# FIXME: This thing is failing because it can't match args that it got
# and calling super, which can't handle it also and fail with ArgumentError
# because super is usual ruby method, can't say what behavior here is
# prefered (keep original ruby, or raise no method error somehow)
# specify {
# expect { Foo.new.greet(1,2,3,4,5,6,7) }.to raise_error(NoMethodError)
# }
specify { expect(subject.options(bar: :baz, one: 1, many: 2)).to eq({bar: :baz, one: 1, many: 2}) }
specify { expect(subject.hashable(:male, {foo: :bar}, :female)).to eq :foo_bar }
specify { expect(subject.hashable(:male, {foo: :baz}, :female)).to eq :baz }
specify { expect(subject.hashable(:male, {foo: 1, bar: 2}, :female)).to eq [1, 2] }
specify { expect(subject.hashable(:male, {foo: 1, baz: 2}, :female)).to eq 1 }
specify { expect(subject.hashable(:male, {bar: :baz}, :female)).to eq :unbound }
specify { expect(subject.hashable(:male, {}, :female)).to eq :empty }
specify { expect(subject.recurse).to eq 'w00t!' }
specify { expect(subject.recurse(:match)).to eq 'w00t!' }
specify { expect(subject.recurse(:super)).to eq 'Hello, World!' }
specify { expect(subject.recurse(:instance)).to eq name }
specify { expect(subject.recurse(:foo)).to eq :foo }
specify { expect(subject.concat(1, 1)).to eq 2 }
specify { expect(subject.concat(1, 'shoe')).to eq '1 shoe' }
specify { expect(subject.concat('shoe', 'fly')).to eq 'shoefly' }
specify { expect(subject.concat(1, 2.9)).to eq 3 }
specify { expect(subject.all(:one, 'a', 'bee', :see)).to eq(['a', 'bee', :see]) }
specify { expect(subject.all(:one, 1, 'bee', :see)).to eq([1, 'bee', :see]) }
specify { expect(subject.all(1, 'a', 'bee', :see)).to eq(['a', ['bee', :see]]) }
specify { expect(subject.all('a', 'bee', :see)).to eq(['a', 'bee', :see]) }
specify { expect { subject.all }.to raise_error(NoMethodError) }
specify { expect(subject.old_enough(20)).to be true }
specify { expect(subject.old_enough(10)).to be false }
specify { expect(subject.right_age(20)).to be true }
specify { expect(subject.right_age(10)).to be false }
specify { expect(subject.right_age(110)).to be false }
specify { expect(subject.wrong_age(20)).to be false }
specify { expect(subject.wrong_age(10)).to be true }
specify { expect(subject.wrong_age(110)).to be true }
context 'inheritance' do
specify { expect(Fizzbuzz.new.greet(:male, 'Jerry')).to eq 'Hello, Mr. Jerry!' }
specify { expect(Fizzbuzz.new.greet(:female, 'Jeri')).to eq 'Hello, Ms. Jeri!' }
specify { expect(Fizzbuzz.new.greet(:unknown, 'Jerry')).to eq 'Hello, Jerry!' }
specify { expect(Fizzbuzz.new.greet(nil, 'Jerry')).to eq 'Goodbye, Jerry!' }
specify { expect(Fizzbuzz.new.who(5)).to eq 15 }
specify { expect(Fizzbuzz.new.who()).to eq 0 }
# FIXME: same issue with Foo's super here
# specify {
# expect {
# Fizzbuzz.new.who('Jerry', 'secret middle name', "D'Antonio")
# }.to raise_error(NoMethodError)
# }
specify { expect(Fizzbuzz.new.boom_boom_room).to eq 'zoom zoom zoom' }
end
end
================================================
FILE: spec/functional/delay_spec.rb
================================================
module Functional
describe Delay do
let!(:fulfilled_value) { 10 }
let!(:rejected_reason) { StandardError.new('mojo jojo') }
let(:pending_subject) do
Delay.new{ fulfilled_value }
end
let(:fulfilled_subject) do
delay = Delay.new{ fulfilled_value }
delay.tap{ delay.value }
end
let(:rejected_subject) do
delay = Delay.new{ raise rejected_reason }
delay.tap{ delay.value }
end
specify{ Functional::Protocol::Satisfy! Delay, :Disposition }
context '#initialize' do
it 'sets the state to :pending' do
expect(Delay.new{ nil }.state).to eq :pending
expect(Delay.new{ nil }).to be_pending
end
it 'raises an exception when no block given' do
expect {
Delay.new
}.to raise_error(ArgumentError)
end
end
context '#state' do
it 'is :pending when first created' do
f = pending_subject
expect(f.state).to eq(:pending)
expect(f).to be_pending
end
it 'is :fulfilled when the handler completes' do
f = fulfilled_subject
expect(f.state).to eq(:fulfilled)
expect(f).to be_fulfilled
end
it 'is :rejected when the handler raises an exception' do
f = rejected_subject
expect(f.state).to eq(:rejected)
expect(f).to be_rejected
end
end
context '#value' do
let(:task){ proc{ nil } }
it 'blocks the caller when :pending and timeout is nil' do
f = pending_subject
expect(f.value).to be_truthy
expect(f).to be_fulfilled
end
it 'is nil when :rejected' do
expected = rejected_subject.value
expect(expected).to be_nil
end
it 'is set to the return value of the block when :fulfilled' do
expected = fulfilled_subject.value
expect(expected).to eq fulfilled_value
end
it 'does not call the block before #value is called' do
expect(task).not_to receive(:call).with(any_args)
Delay.new(&task)
end
it 'calls the block when #value is called' do
expect(task).to receive(:call).once.with(any_args).and_return(nil)
Delay.new(&task).value
end
it 'only calls the block once no matter how often #value is called' do
expect(task).to receive(:call).once.with(any_args).and_return(nil)
delay = Delay.new(&task)
5.times{ delay.value }
end
end
context '#reason' do
it 'is nil when :pending' do
expect(pending_subject.reason).to be_nil
end
it 'is nil when :fulfilled' do
expect(fulfilled_subject.reason).to be_nil
end
it 'is set to error object of the exception when :rejected' do
expect(rejected_subject.reason).to be_a(Exception)
expect(rejected_subject.reason.to_s).to match(/#{rejected_reason}/)
end
end
context 'predicates' do
specify '#value? returns true when :fulfilled' do
expect(pending_subject).to_not be_value
expect(fulfilled_subject).to be_value
expect(rejected_subject).to_not be_value
end
specify '#reason? returns true when :rejected' do
expect(pending_subject).to_not be_reason
expect(fulfilled_subject).to_not be_reason
expect(rejected_subject).to be_reason
end
specify '#fulfilled? returns true when :fulfilled' do
expect(pending_subject).to_not be_fulfilled
expect(fulfilled_subject).to be_fulfilled
expect(rejected_subject).to_not be_fulfilled
end
specify '#rejected? returns true when :rejected' do
expect(pending_subject).to_not be_rejected
expect(fulfilled_subject).to_not be_rejected
expect(rejected_subject).to be_rejected
end
specify '#pending? returns true when :pending' do
expect(pending_subject).to be_pending
expect(fulfilled_subject).to_not be_pending
expect(rejected_subject).to_not be_pending
end
end
end
end
================================================
FILE: spec/functional/either_spec.rb
================================================
require_relative 'abstract_struct_shared'
module Functional
describe Either do
let!(:value){ 42 }
let!(:reason){ StandardError.new }
let!(:expected_fields){ [:left, :right] }
let!(:expected_values){ [value, nil] }
let(:struct_class) { Either }
let(:struct_object) { Either.left(value) }
let(:other_object) { Either.left(Object.new) }
let(:left_subject){ Either.left(reason) }
let(:right_subject){ Either.right(value) }
it_should_behave_like :abstract_struct
specify{ Functional::Protocol::Satisfy! Either, :Either }
specify{ Functional::Protocol::Satisfy! Either, :Disposition }
context 'initialization' do
it 'cannot be constructed directly' do
expect {
Either.new
}.to raise_error(NameError)
end
it 'sets the left value when constructed by #left' do
expect(Either.left(value).left).to eq value
end
it 'sets the right value when constructed by #right' do
expect(Either.right(value).right).to eq value
end
it 'freezes the new object' do
expect(Either.left(:foo)).to be_frozen
expect(Either.right(:foo)).to be_frozen
end
it 'aliases #left to #reason' do
expect(Either.reason(value).left).to eq value
end
it 'aliases #right to #value' do
expect(Either.value(value).right).to eq value
end
context '#error' do
it 'sets left to a StandardError with backtrace when no arguments given' do
either = Either.error
expect(either.left).to be_a StandardError
expect(either.left.message).to_not be nil
expect(either.left.backtrace).to_not be_empty
end
it 'sets left to a StandardError with the given message' do
message = 'custom error message'
either = Either.error(message)
expect(either.left).to be_a StandardError
expect(either.left.message).to eq message
expect(either.left.backtrace).to_not be_empty
end
it 'sets left to an object of the given class with the given message' do
message = 'custom error message'
error_class = ArgumentError
either = Either.error(message, error_class)
expect(either.left).to be_a error_class
expect(either.left.message).to eq message
expect(either.left.backtrace).to_not be_empty
end
end
end
context 'state' do
specify '#left? returns true when the left value is set' do
expect(left_subject).to be_left
end
specify '#left? returns false when the right value is set' do
expect(right_subject).to_not be_left
end
specify '#right? returns true when the right value is set' do
expect(right_subject).to be_right
end
specify '#right? returns false when the left value is set' do
expect(left_subject).to_not be_right
end
specify '#left returns the left value when left is set' do
expect(left_subject.left).to eq reason
end
specify '#left returns nil when right is set' do
expect(right_subject.left).to be_nil
end
specify '#right returns the right value when right is set' do
expect(right_subject.right).to eq value
end
specify '#right returns nil when left is set' do
expect(left_subject.right).to be_nil
end
specify 'aliases #left? as #reason?' do
expect(left_subject.reason?).to be true
end
specify 'aliases #right? as #value?' do
expect(right_subject.value?).to be true
end
specify 'aliases #left as #reason' do
expect(left_subject.reason).to eq reason
expect(right_subject.reason).to be_nil
end
specify 'aliases #right as #value' do
expect(right_subject.value).to eq value
expect(left_subject.value).to be_nil
end
end
context '#swap' do
it 'converts a left projection into a right projection' do
subject = Either.left(:foo)
swapped = subject.swap
expect(swapped).to be_right
expect(swapped.left).to be_nil
expect(swapped.right).to eq :foo
end
it 'converts a right projection into a left projection' do
subject = Either.right(:foo)
swapped = subject.swap
expect(swapped).to be_left
expect(swapped.right).to be_nil
expect(swapped.left).to eq :foo
end
end
context '#either' do
it 'passes the left value to the left proc when left' do
expected = nil
subject = Either.left(100)
subject.either(
->(left) { expected = left },
->(right) { expected = -1 }
)
expect(expected).to eq 100
end
it 'returns the value of the left proc when left' do
subject = Either.left(100)
expect(
subject.either(
->(left) { left * 2 },
->(right) { nil }
)
).to eq 200
end
it 'passes the right value to the right proc when right' do
expected = nil
subject = Either.right(100)
subject.either(
->(right) { expected = -1 },
->(right) { expected = right }
)
expect(expected).to eq 100
end
it 'returns the value of the right proc when right' do
subject = Either.right(100)
expect(
subject.either(
->(right) { nil },
->(right) { right * 2 }
)
).to eq 200
end
end
context '#iff' do
it 'returns a lefty with the given left value when the boolean is true' do
subject = Either.iff(:foo, :bar, true)
expect(subject).to be_left
expect(subject.left).to eq :foo
end
it 'returns a righty with the given right value when the boolean is false' do
subject = Either.iff(:foo, :bar, false)
expect(subject).to be_right
expect(subject.right).to eq :bar
end
it 'returns a lefty with the given left value when the block is truthy' do
subject = Either.iff(:foo, :bar){ :baz }
expect(subject).to be_left
expect(subject.left).to eq :foo
end
it 'returns a righty with the given right value when the block is false' do
subject = Either.iff(:foo, :bar){ false }
expect(subject).to be_right
expect(subject.right).to eq :bar
end
it 'returns a righty with the given right value when the block is nil' do
subject = Either.iff(:foo, :bar){ nil }
expect(subject).to be_right
expect(subject.right).to eq :bar
end
it 'raises an exception when both a boolean and a block are given' do
expect {
subject = Either.iff(:foo, :bar, true){ nil }
}.to raise_error(ArgumentError)
end
end
end
end
================================================
FILE: spec/functional/final_struct_spec.rb
================================================
require 'ostruct'
module Functional
describe FinalStruct do
context 'instanciation' do
specify 'with no args defines no fields' do
subject = FinalStruct.new
expect(subject.to_h).to be_empty
end
specify 'with a hash sets fields using has values' do
subject = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three')
expect(subject.foo).to eq 1
expect(subject.bar).to eq :two
expect(subject.baz).to eq 'three'
end
specify 'with a hash creates true predicates for has keys' do
subject = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three')
expect(subject.foo?).to be true
expect(subject.bar?).to be true
expect(subject.baz?).to be true
end
specify 'can be created from any object that responds to #to_h' do
clazz = Class.new do
def to_h; {answer: 42, harmless: 'mostly'}; end
end
struct = clazz.new
subject = FinalStruct.new(struct)
expect(subject.answer).to eq 42
expect(subject.harmless).to eq 'mostly'
end
specify 'raises an exception if given a non-hash argument' do
expect {
FinalStruct.new(:bogus)
}.to raise_error(ArgumentError)
end
end
context 'set fields' do
subject do
struct = FinalStruct.new
struct.foo = 42
struct.bar = "Don't Panic"
struct
end
specify 'have a reader which returns the value' do
expect(subject.foo).to eq 42
expect(subject.bar).to eq "Don't Panic"
end
specify 'have a predicate which returns true' do
expect(subject.foo?).to be true
expect(subject.bar?).to be true
end
specify 'raise an exception when written to again' do
expect {subject.foo = 0}.to raise_error(Functional::FinalityError)
expect {subject.bar = 0}.to raise_error(Functional::FinalityError)
end
end
context 'unset fields' do
subject { FinalStruct.new }
specify 'have a magic reader that always returns nil' do
expect(subject.foo).to be nil
expect(subject.bar).to be nil
expect(subject.baz).to be nil
end
specify 'have a magic predicate that always returns false' do
expect(subject.foo?).to be false
expect(subject.bar?).to be false
expect(subject.baz?).to be false
end
specify 'have a magic writer that sets the field' do
expect(subject.foo = 42).to eq 42
expect(subject.bar = :towel).to eq :towel
expect(subject.baz = "Don't Panic").to eq "Don't Panic"
end
end
context 'accessors' do
let!(:field_value_pairs) { {foo: 1, bar: :two, baz: 'three'} }
subject { FinalStruct.new(field_value_pairs) }
specify '#get returns the value of a set field' do
expect(subject.get(:foo)).to eq 1
end
specify '#get returns nil for an unset field' do
expect(subject.get(:bogus)).to be nil
end
specify '#[] is an alias for #get' do
expect(subject[:foo]).to eq 1
expect(subject[:bogus]).to be nil
end
specify '#set sets the value of an unset field' do
subject.set(:harmless, 'mostly')
expect(subject.harmless).to eq 'mostly'
expect(subject.harmless?).to be true
end
specify '#set raises an exception if the field has already been set' do
subject.set(:harmless, 'mostly')
expect {
subject.set(:harmless, 'extremely')
}.to raise_error(Functional::FinalityError)
end
specify '#[]= is an alias for set' do
subject[:harmless] = 'mostly'
expect(subject.harmless).to eq 'mostly'
expect {
subject[:harmless] = 'extremely'
}.to raise_error(Functional::FinalityError)
end
specify '#set? returns false for an unset field' do
expect(subject.set?(:harmless)).to be false
end
specify '#set? returns true for a field that has been set' do
subject.set(:harmless, 'mostly')
expect(subject.set?(:harmless)).to be true
end
specify '#get_or_set returns the value of a set field' do
subject.answer = 42
expect(subject.get_or_set(:answer, 100)).to eq 42
end
specify '#get_or_set sets the value of an unset field' do
subject.get_or_set(:answer, 42)
expect(subject.answer).to eq 42
expect(subject.answer?).to be true
end
specify '#get_or_set returns the value of a newly set field' do
expect(subject.get_or_set(:answer, 42)).to eq 42
end
specify '#fetch gets the value of a set field' do
subject.harmless = 'mostly'
expect(subject.fetch(:harmless, 'extremely')).to eq 'mostly'
end
specify '#fetch returns the given value when the field is unset' do
expect(subject.fetch(:harmless, 'extremely')).to eq 'extremely'
end
specify '#fetch does not set an unset field' do
subject.fetch(:answer, 42)
expect(subject.answer).to be_nil
expect(subject.answer?).to be false
end
specify '#to_h returns the key/value pairs for all set values' do
subject = FinalStruct.new(field_value_pairs)
expect(subject.to_h).to eq field_value_pairs
end
specify '#to_h is updated when new fields are added' do
subject = FinalStruct.new
field_value_pairs.each_pair do |field, value|
subject.set(field, value)
end
expect(subject.to_h).to eq field_value_pairs
end
specify '#each_pair returns an Enumerable when no block given' do
subject = FinalStruct.new(field_value_pairs)
expect(subject.each_pair).to be_a Enumerable
end
specify '#each_pair enumerates over each field/value pair' do
subject = FinalStruct.new(field_value_pairs)
result = {}
subject.each_pair do |field, value|
result[field] = value
end
expect(result).to eq field_value_pairs
end
end
context 'reflection' do
specify '#eql? returns true when both define the same fields with the same values' do
first = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three')
second = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three')
expect(first.eql?(second)).to be true
expect(first == second).to be true
end
specify '#eql? returns false when other has different fields defined' do
first = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three')
second = FinalStruct.new(foo: 1, 'bar' => :two)
expect(first.eql?(second)).to be false
expect(first == second).to be false
end
specify '#eql? returns false when other has different field values' do
first = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three')
second = FinalStruct.new(foo: 1, 'bar' => :two, baz: 3)
expect(first.eql?(second)).to be false
expect(first == second).to be false
end
specify '#eql? returns false when other is not a FinalStruct' do
attributes = {answer: 42, harmless: 'mostly'}
clazz = Class.new do
def to_h; {answer: 42, harmless: 'mostly'}; end
end
other = clazz.new
subject = FinalStruct.new(attributes)
expect(subject.eql?(other)).to be false
expect(subject == other).to be false
end
specify '#inspect begins with the class name' do
subject = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three')
expect(subject.inspect).to match(/^#<#{described_class}\s+/)
end
specify '#inspect includes all field/value pairs' do
field_value_pairs = {foo: 1, 'bar' => :two, baz: 'three'}
subject = FinalStruct.new(field_value_pairs)
field_value_pairs.each do |field, value|
expect(subject.inspect).to match(/:#{field}=>"?:?#{value}"?/)
end
end
specify '#to_s returns the same value as #inspect' do
subject = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three')
expect(subject.to_s).to eq subject.inspect
end
specify '#method_missing raises an exception for methods with unrecognized signatures' do
expect {
subject.foo(1, 2, 3)
}.to raise_error(NoMethodError)
end
end
end
end
================================================
FILE: spec/functional/final_var_spec.rb
================================================
module Functional
describe FinalVar do
context 'instanciation' do
it 'is unset when no arguments given' do
expect(FinalVar.new).to_not be_set
end
it 'is set with the given argument' do
expect(FinalVar.new(41)).to be_set
end
end
context '#get' do
subject { FinalVar.new }
it 'returns nil when unset' do
expect(subject.get).to be nil
end
it 'returns the value when set' do
expect(FinalVar.new(42).get).to eq 42
end
it 'is aliased as #value' do
expect(subject.value).to be nil
subject.set(42)
expect(subject.value).to eq 42
end
end
context '#set' do
subject { FinalVar.new }
it 'sets the value when unset' do
subject.set(42)
expect(subject.get).to eq 42
end
it 'returns the new value when unset' do
expect(subject.set(42)).to eq 42
end
it 'raises an exception when already set' do
subject.set(42)
expect {
subject.set(42)
}.to raise_error(Functional::FinalityError)
end
it 'is aliased as #value=' do
subject.value = 42
expect(subject.get).to eq 42
end
end
context '#set?' do
it 'returns false when unset' do
expect(FinalVar.new).to_not be_set
end
it 'returns true when set' do
expect(FinalVar.new(42)).to be_set
end
it 'is aliased as value?' do
expect(FinalVar.new.value?).to be false
expect(FinalVar.new(42).value?).to be true
end
end
context '#get_or_set' do
it 'sets the value when unset' do
subject = FinalVar.new
subject.get_or_set(42)
expect(subject.get).to eq 42
end
it 'returns the new value when previously unset' do
subject = FinalVar.new
expect(subject.get_or_set(42)).to eq 42
end
it 'returns the current value when already set' do
subject = FinalVar.new(100)
expect(subject.get_or_set(42)).to eq 100
end
end
context '#fetch' do
it 'returns the given default value when unset' do
subject = FinalVar.new
expect(subject.fetch(42)).to eq 42
end
it 'does not change the current value when unset' do
subject = FinalVar.new
subject.fetch(42)
expect(subject.get).to be nil
end
it 'returns the current value when already set' do
subject = FinalVar.new(100)
expect(subject.get_or_set(42)).to eq 100
end
end
context 'reflection' do
specify '#eql? returns false when unset' do
expect(FinalVar.new.eql?(nil)).to be false
expect(FinalVar.new.eql?(42)).to be false
expect(FinalVar.new.eql?(FinalVar.new.value)).to be false
end
specify '#eql? returns false when set and the value does not match other' do
subject = FinalVar.new(42)
expect(subject.eql?(100)).to be false
end
specify '#eql? returns true when set and the value matches other' do
subject = FinalVar.new(42)
expect(subject.eql?(42)).to be true
end
specify '#eql? returns true when set and other is a FinalVar with the same value' do
subject = FinalVar.new(42)
other = FinalVar.new(42)
expect(subject.eql?(other)).to be true
end
specify 'aliases #== as #eql?' do
expect(FinalVar.new == nil).to be false
expect(FinalVar.new == 42).to be false
expect(FinalVar.new == FinalVar.new).to be false
expect(FinalVar.new(42) == 42).to be true
expect(FinalVar.new(42) == FinalVar.new(42)).to be true
end
specify '#inspect includes the word "value" and the value when set' do
subject = FinalVar.new(42)
expect(subject.inspect).to match(/value\s?=\s?42\s*>$/)
end
specify '#inspect include the word "unset" when unset' do
subject = FinalVar.new
expect(subject.inspect).to match(/unset\s*>$/i)
end
specify '#to_s returns nil as a string when unset' do
expect(FinalVar.new.to_s).to eq nil.to_s
end
specify '#to_s returns the value as a string when set' do
expect(FinalVar.new(42).to_s).to eq 42.to_s
expect(FinalVar.new('42').to_s).to eq '42'
end
end
end
end
================================================
FILE: spec/functional/memo_spec.rb
================================================
module Functional
describe Memo do
def create_new_memo_class
Class.new do
include Functional::Memo
class << self
attr_accessor :count
end
self.count = 0
def self.add(a, b)
self.count += 1
a + b
end
memoize :add
def self.increment(n)
self.count += 1
end
def self.exception(ex = StandardError)
raise ex
end
end
end
subject{ create_new_memo_class }
context 'specification' do
it 'raises an exception when the method is not defined' do
expect {
subject.memoize(:bogus)
}.to raise_error(NameError)
end
it 'raises an exception when the given method has already been memoized' do
expect{
subject.memoize(:add)
}.to raise_error(ArgumentError)
end
it 'allocates a different cache for each class/module' do
class_1 = create_new_memo_class
class_2 = create_new_memo_class
10.times do
class_1.add(0, 0)
class_2.add(0, 0)
end
expect(class_1.count).to eq 1
expect(class_2.count).to eq 1
end
it 'works when included in a class' do
subject = Class.new do
include Functional::Memo
class << self
attr_accessor :count
end
self.count = 0
def self.foo
self.count += 1
end
memoize :foo
end
10.times{ subject.foo }
expect(subject.count).to eq 1
end
it 'works when included in a module' do
subject = Module.new do
include Functional::Memo
class << self
attr_accessor :count
end
self.count = 0
def self.foo
self.count += 1
end
memoize :foo
end
10.times{ subject.foo }
expect(subject.count).to eq 1
end
it 'works when extended by a module' do
subject = Module.new do
extend Functional::Memo
class << self
attr_accessor :count
end
self.count = 0
def self.foo
self.count += 1
end
memoize :foo
end
10.times{ subject.foo }
expect(subject.count).to eq 1
end
end
context 'caching behavior' do
it 'calls the real method on first instance of given args' do
subject.add(1, 2)
expect(subject.count).to eq 1
end
it 'calls the real method on first instance of given args' do
subject.add(1, 2)
expect(subject.count).to eq 1
end
it 'uses the memo on second instance of given args' do
5.times { subject.add(1, 2) }
expect(subject.count).to eq 1
end
it 'calls the real method when given a block' do
5.times { subject.add(1, 2){ nil } }
expect(subject.count).to eq 5
end
it 'raises an exception when arity does not match' do
expect {
subject.add
}.to raise_error(ArgumentError)
end
end
context 'maximum cache size' do
it 'raises an exception when given a non-positive :at_most' do
expect {
subject.memoize(:increment, at_most: -1)
}.to raise_error(ArgumentError)
end
it 'sets no limit when :at_most not given' do
subject.memoize(:increment)
10000.times{|i| subject.increment(i) }
expect(subject.count).to eq 10000
end
it 'calls the real method when the :at_most size is reached' do
subject.memoize(:increment, at_most: 5)
10000.times{|i| subject.increment(i % 10) }
expect(subject.count).to eq 5005
end
end
context 'thread safety' do
let(:memoizer_factory){ Functional::Memo::ClassMethods.const_get(:Memoizer) }
let(:memoizer){ memoizer_factory.new(:func, 0) }
before(:each) do
allow(memoizer_factory).to receive(:new).with(any_args).and_return(memoizer)
end
it 'locks a mutex whenever a memoized function is called' do
expect(memoizer).to receive(:synchronize).exactly(:once).with(no_args)
subject.memoize(:increment)
subject.increment(0)
end
it 'unlocks the mutex whenever a memoized function is called' do
expect(memoizer).to receive(:synchronize).exactly(:once).with(no_args)
subject.memoize(:increment)
subject.increment(0)
end
it 'unlocks the mutex when the method call raises an exception' do
expect(memoizer).to receive(:synchronize).exactly(:once).with(no_args)
subject.memoize(:exception)
begin
subject.exception
rescue
# suppress
end
end
it 'uses different mutexes for different functions' do
expect(memoizer_factory).to receive(:new).with(any_args).exactly(3).times.and_return(memoizer)
# once for memoize(:add) in the definition
subject.memoize(:increment)
subject.memoize(:exception)
end
end
end
end
================================================
FILE: spec/functional/option_spec.rb
================================================
require_relative 'abstract_struct_shared'
require 'securerandom'
module Functional
describe Option do
let!(:value){ 42 }
let!(:expected_fields){ [:some] }
let!(:expected_values){ [value] }
let(:struct_class) { Option }
let(:struct_object) { Option.some(value) }
let(:other_object) { Option.some(Object.new) }
let(:some_subject){ Option.some(value) }
let(:none_subject){ Option.none }
it_should_behave_like :abstract_struct
specify{ Functional::Protocol::Satisfy! Option, :Option }
specify{ Functional::Protocol::Satisfy! Option, :Disposition }
let(:some_value){ SecureRandom.uuid }
let(:other_value){ SecureRandom.uuid }
context 'initialization' do
it 'cannot be constructed directly' do
expect {
Option.new
}.to raise_error(NameError)
end
it 'sets the value when constructed by #some' do
expect(Option.some(value).some).to eq value
end
it 'sets the value to nil when constructed by #none' do
expect(Option.none.some).to be_nil
end
it 'sets the reason to nil when constructed by #none' do
expect(Option.none.reason).to be_nil
end
it 'sets the optional reason when constructed by #none' do
reason = 'foobar'
expect(Option.none(reason).reason).to eq reason
end
it 'freezes the new object' do
expect(Option.some(:foo)).to be_frozen
expect(Option.none).to be_frozen
end
end
context 'state' do
specify '#some? returns true when the some value is set' do
expect(some_subject).to be_some
gitextract_u_5mpmcl/
├── .coveralls.yml
├── .gitignore
├── .rspec
├── .travis.yml
├── .yardopts
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE
├── README.md
├── Rakefile
├── appveyor.yml
├── doc/
│ ├── memo.md
│ ├── memoize.rb
│ ├── pattern_matching.md
│ ├── protocol.md
│ └── record.md
├── functional_ruby.gemspec
├── lib/
│ ├── functional/
│ │ ├── abstract_struct.rb
│ │ ├── delay.rb
│ │ ├── either.rb
│ │ ├── final_struct.rb
│ │ ├── final_var.rb
│ │ ├── memo.rb
│ │ ├── method_signature.rb
│ │ ├── option.rb
│ │ ├── pattern_matching.rb
│ │ ├── protocol.rb
│ │ ├── protocol_info.rb
│ │ ├── record.rb
│ │ ├── synchronization.rb
│ │ ├── tuple.rb
│ │ ├── type_check.rb
│ │ ├── union.rb
│ │ ├── value_struct.rb
│ │ └── version.rb
│ └── functional.rb
├── spec/
│ ├── .gitignore
│ ├── functional/
│ │ ├── abstract_struct_shared.rb
│ │ ├── complex_pattern_matching_spec.rb
│ │ ├── delay_spec.rb
│ │ ├── either_spec.rb
│ │ ├── final_struct_spec.rb
│ │ ├── final_var_spec.rb
│ │ ├── memo_spec.rb
│ │ ├── option_spec.rb
│ │ ├── pattern_matching_spec.rb
│ │ ├── protocol_info_spec.rb
│ │ ├── protocol_spec.rb
│ │ ├── record_spec.rb
│ │ ├── tuple_spec.rb
│ │ ├── type_check_spec.rb
│ │ ├── union_spec.rb
│ │ └── value_struct_spec.rb
│ ├── spec_helper.rb
│ └── support/
│ └── .gitignore
└── tasks/
├── .gitignore
├── metrics.rake
└── update_doc.rake
SYMBOL INDEX (339 symbols across 35 files)
FILE: doc/memoize.rb
class Factors (line 6) | class Factors
method sum_of (line 9) | def self.sum_of(number)
method of (line 13) | def self.of(number)
method factor? (line 17) | def self.factor?(number, potential)
method perfect? (line 21) | def self.perfect?(number)
method abundant? (line 25) | def self.abundant?(number)
method deficient? (line 29) | def self.deficient?(number)
function memory_usage (line 40) | def memory_usage
function print_memory_usage (line 44) | def print_memory_usage
function run_benchmark (line 49) | def run_benchmark(n = 10000)
FILE: lib/functional.rb
type Functional (line 27) | module Functional
FILE: lib/functional/abstract_struct.rb
type Functional (line 12) | module Functional
type AbstractStruct (line 16) | module AbstractStruct
function each (line 27) | def each
function each_pair (line 41) | def each_pair
function eql? (line 53) | def eql?(other)
function inspect (line 64) | def inspect
function length (line 73) | def length
function fields (line 81) | def fields
function to_h (line 88) | def to_h
function set_data_hash (line 98) | def set_data_hash(data)
function set_values_array (line 106) | def set_values_array(values)
function define_class (line 121) | def self.define_class(parent, datatype, fields)
function included (line 135) | def self.included(base)
type ClassMethods (line 143) | module ClassMethods
FILE: lib/functional/delay.rb
type Functional (line 3) | module Functional
class Delay (line 30) | class Delay < Synchronization::Object
method initialize (line 37) | def initialize(&block)
method state (line 49) | def state
method reason (line 58) | def reason
method value (line 75) | def value
method fulfilled? (line 81) | def fulfilled?
method rejected? (line 88) | def rejected?
method pending? (line 95) | def pending?
method execute_task_once (line 107) | def execute_task_once
FILE: lib/functional/either.rb
type Functional (line 12) | module Functional
class Either (line 84) | class Either < Synchronization::Object
method left (line 101) | def left(value)
method right (line 110) | def right(value)
method error (line 133) | def error(message = nil, clazz = StandardError)
method left (line 143) | def left
method right (line 151) | def right
method left? (line 159) | def left?
method right? (line 168) | def right?
method swap (line 177) | def swap
method either (line 190) | def either(lproc, rproc)
method iff (line 204) | def self.iff(lvalue, rvalue, condition = NO_VALUE)
method initialize (line 218) | def initialize(value, is_left)
FILE: lib/functional/final_struct.rb
type Functional (line 4) | module Functional
class FinalStruct (line 48) | class FinalStruct < Synchronization::Object
method initialize (line 55) | def initialize(attributes = {})
method get (line 72) | def get(field)
method set (line 90) | def set(field, value)
method set? (line 108) | def set?(field)
method get_or_set (line 118) | def get_or_set(field, value)
method fetch (line 128) | def fetch(field, default)
method each_pair (line 139) | def each_pair
method to_h (line 152) | def to_h
method eql? (line 162) | def eql?(other)
method inspect (line 172) | def inspect
method ns_get_attribute (line 182) | def ns_get_attribute(field)
method ns_set_attribute (line 188) | def ns_set_attribute(field, value)
method ns_attribute_has_been_set? (line 194) | def ns_attribute_has_been_set?(field)
method method_missing (line 208) | def method_missing(symbol, *args)
FILE: lib/functional/final_var.rb
type Functional (line 3) | module Functional
class FinalVar (line 44) | class FinalVar < Synchronization::Object
method initialize (line 53) | def initialize(value = NO_VALUE)
method get (line 61) | def get
method set (line 71) | def set(value)
method set? (line 85) | def set?
method get_or_set (line 94) | def get_or_set(value)
method fetch (line 108) | def fetch(default)
method eql? (line 120) | def eql?(other)
method inspect (line 136) | def inspect
method to_s (line 150) | def to_s
method has_been_set? (line 158) | def has_been_set?
FILE: lib/functional/memo.rb
type Functional (line 3) | module Functional
type Memo (line 18) | module Memo
function extended (line 21) | def self.extended(base)
function included (line 28) | def self.included(base)
type ClassMethods (line 35) | module ClassMethods
class Memoizer (line 38) | class Memoizer < Synchronization::Object
method initialize (line 40) | def initialize(function, max_cache)
method max_cache? (line 48) | def max_cache?
function memoize (line 71) | def memoize(func, opts = {})
function __define_memo_proxy__ (line 82) | def __define_memo_proxy__(func)
function __proxy_memoized_method__ (line 91) | def __proxy_memoized_method__(func, *args, &block)
FILE: lib/functional/method_signature.rb
type Functional (line 1) | module Functional
type PatternMatching (line 3) | module PatternMatching
type MethodSignature (line 9) | module MethodSignature
function match? (line 15) | def match?(pattern, args)
function valid_pattern? (line 33) | def valid_pattern?(args, pattern)
function all_param_and_last_arg? (line 41) | def all_param_and_last_arg?(pattern, param, index)
function arg_is_type_of_param? (line 49) | def arg_is_type_of_param?(param, arg)
function hash_param_with_matching_arg? (line 57) | def hash_param_with_matching_arg?(param, arg)
function param_matches_arg? (line 67) | def param_matches_arg?(param, arg)
FILE: lib/functional/option.rb
type Functional (line 12) | module Functional
class Option (line 21) | class Option < Synchronization::Object
method none (line 41) | def none(reason = nil)
method some (line 49) | def some(value)
method some? (line 57) | def some?
method none? (line 66) | def none?
method some (line 75) | def some
method length (line 85) | def length
method and (line 106) | def and(other = NO_OPTION)
method or (line 134) | def or(other = NO_OPTION)
method else (line 158) | def else(other = NO_OPTION)
method iff (line 180) | def self.iff(value, condition = NO_OPTION)
method inspect (line 187) | def inspect
method initialize (line 201) | def initialize(value, none, reason = nil)
FILE: lib/functional/pattern_matching.rb
type Functional (line 3) | module Functional
type PatternMatching (line 15) | module PatternMatching
function initialize (line 30) | def initialize(function, clazz, pattern)
function when (line 35) | def when(&block)
function __unbound_args__ (line 50) | def __unbound_args__(match, args)
function __pass_guard__? (line 68) | def __pass_guard__?(matcher, args)
function __pattern_match__ (line 74) | def __pattern_match__(clazz, function, *args, &block)
function included (line 83) | def self.included(base)
type ClassMethods (line 90) | module ClassMethods
function _ (line 93) | def _()
function defn (line 98) | def defn(function, *args, &block)
function __define_method_with_matching__ (line 125) | def __define_method_with_matching__(function)
function __function_pattern_matches__ (line 147) | def __function_pattern_matches__
function __register_pattern__ (line 152) | def __register_pattern__(function, *args, &block)
function __pattern_arity__ (line 162) | def __pattern_arity__(pat)
FILE: lib/functional/protocol.rb
type Functional (line 3) | module Functional
function SpecifyProtocol (line 38) | def SpecifyProtocol(name, &block)
type Protocol (line 60) | module Protocol
function Satisfy? (line 72) | def Satisfy?(target, *protocols)
function Satisfy! (line 87) | def Satisfy!(target, *protocols)
function Specified? (line 100) | def Specified?(*protocols)
function Specified! (line 115) | def Specified!(*protocols)
function satisfies? (line 129) | def self.satisfies?(target, protocol)
function unspecified (line 138) | def self.unspecified(*protocols)
function error (line 151) | def self.error(target, message, *protocols)
FILE: lib/functional/protocol_info.rb
type Functional (line 3) | module Functional
class ProtocolInfo (line 9) | class ProtocolInfo < Synchronization::Object
method initialize (line 22) | def initialize(name, &specification)
method instance_methods (line 39) | def instance_methods
method class_methods (line 47) | def class_methods
method constants (line 54) | def constants
method satisfies? (line 61) | def satisfies?(target)
method satisfies_constants? (line 77) | def satisfies_constants?(target)
method satisfies_instance_methods? (line 86) | def satisfies_instance_methods?(target)
method satisfies_class_methods? (line 101) | def satisfies_class_methods?(target)
method check_arity? (line 121) | def check_arity?(method, expected)
method instance_method (line 133) | def instance_method(name, arity = nil)
method class_method (line 142) | def class_method(name, arity = nil)
method attr_reader (line 150) | def attr_reader(name)
method attr_writer (line 157) | def attr_writer(name)
method attr_accessor (line 164) | def attr_accessor(name)
method class_attr_reader (line 172) | def class_attr_reader(name)
method class_attr_writer (line 179) | def class_attr_writer(name)
method class_attr_accessor (line 186) | def class_attr_accessor(name)
method constant (line 194) | def constant(name)
FILE: lib/functional/record.rb
type Functional (line 5) | module Functional
type Record (line 25) | module Record
function new (line 33) | def new(*fields, &block)
class Restrictions (line 62) | class Restrictions
method initialize (line 73) | def initialize(types = nil, &block)
method mandatory (line 86) | def mandatory(*fields)
method default (line 94) | def default(field, value)
method clone_default (line 104) | def clone_default(field)
method validate! (line 117) | def validate!(data)
method validate_mandatory! (line 129) | def validate_mandatory!(data)
method validate_types! (line 139) | def validate_types!(data)
method uncloneable? (line 156) | def uncloneable?(object)
function check_types! (line 166) | def check_types!(types)
function build (line 179) | def build(name, fields, types, &block)
function define_initializer (line 194) | def define_initializer(record)
function define_reader (line 216) | def define_reader(record, field)
FILE: lib/functional/synchronization.rb
type Functional (line 1) | module Functional
type Synchronization (line 7) | module Synchronization
class Object (line 14) | class Object
method initialize (line 17) | def initialize(*args)
method synchronize (line 23) | def synchronize
method ensure_ivar_visibility! (line 28) | def ensure_ivar_visibility!
method initialize (line 39) | def initialize(*args)
method synchronize (line 45) | def synchronize(&block)
method ensure_ivar_visibility! (line 50) | def ensure_ivar_visibility!
method initialize (line 64) | def initialize(*args)
method synchronize (line 72) | def synchronize
method ensure_ivar_visibility! (line 81) | def ensure_ivar_visibility!
class Object (line 36) | class Object
method initialize (line 17) | def initialize(*args)
method synchronize (line 23) | def synchronize
method ensure_ivar_visibility! (line 28) | def ensure_ivar_visibility!
method initialize (line 39) | def initialize(*args)
method synchronize (line 45) | def synchronize(&block)
method ensure_ivar_visibility! (line 50) | def ensure_ivar_visibility!
method initialize (line 64) | def initialize(*args)
method synchronize (line 72) | def synchronize
method ensure_ivar_visibility! (line 81) | def ensure_ivar_visibility!
class Object (line 61) | class Object
method initialize (line 17) | def initialize(*args)
method synchronize (line 23) | def synchronize
method ensure_ivar_visibility! (line 28) | def ensure_ivar_visibility!
method initialize (line 39) | def initialize(*args)
method synchronize (line 45) | def synchronize(&block)
method ensure_ivar_visibility! (line 50) | def ensure_ivar_visibility!
method initialize (line 64) | def initialize(*args)
method synchronize (line 72) | def synchronize
method ensure_ivar_visibility! (line 81) | def ensure_ivar_visibility!
FILE: lib/functional/tuple.rb
type Functional (line 3) | module Functional
class Tuple (line 24) | class Tuple < Synchronization::Object
method initialize (line 30) | def initialize(data = [])
method at (line 45) | def at(index)
method fetch (line 60) | def fetch(index, default)
method length (line 71) | def length
method intersect (line 82) | def intersect(other)
method union (line 91) | def union(other)
method concat (line 100) | def concat(other)
method diff (line 109) | def diff(other)
method repeat (line 120) | def repeat(multiple)
method uniq (line 130) | def uniq
method each (line 139) | def each
method each_with_index (line 152) | def each_with_index
method sequence (line 169) | def sequence
method eql? (line 188) | def eql?(other)
method empty? (line 196) | def empty?
method first (line 203) | def first
method rest (line 212) | def rest
method to_a (line 225) | def to_a
method inspect (line 235) | def inspect
method to_s (line 245) | def to_s
FILE: lib/functional/type_check.rb
type Functional (line 1) | module Functional
type TypeCheck (line 6) | module TypeCheck
function Type? (line 15) | def Type?(value, *types)
function Type! (line 29) | def Type!(value, *types)
function Match? (line 45) | def Match?(value, *types)
function Match! (line 62) | def Match!(value, *types)
function Child? (line 76) | def Child?(value, *types)
function Child! (line 90) | def Child!(value, *types)
function error (line 107) | def self.error(value, message, types)
FILE: lib/functional/union.rb
type Functional (line 4) | module Functional
type Union (line 56) | module Union
function new (line 63) | def new(*fields)
function build (line 75) | def build(fields)
function define_properties (line 92) | def define_properties(union)
function define_predicate (line 103) | def define_predicate(union, field)
function define_reader (line 115) | def define_reader(union, field)
function define_initializer (line 126) | def define_initializer(union)
function define_factory (line 148) | def define_factory(union, field)
FILE: lib/functional/value_struct.rb
type Functional (line 3) | module Functional
class ValueStruct (line 28) | class ValueStruct < Synchronization::Object
method initialize (line 30) | def initialize(attributes)
method get (line 46) | def get(field)
method set? (line 56) | def set?(field)
method fetch (line 66) | def fetch(field, default)
method each_pair (line 77) | def each_pair
method to_h (line 88) | def to_h
method eql? (line 98) | def eql?(other)
method inspect (line 108) | def inspect
method set_attribute (line 123) | def set_attribute(field, value)
method method_missing (line 136) | def method_missing(symbol, *args)
FILE: lib/functional/version.rb
type Functional (line 1) | module Functional
FILE: spec/functional/complex_pattern_matching_spec.rb
class Bar (line 3) | class Bar
method greet (line 4) | def greet
class Foo (line 9) | class Foo < Bar
class Baz (line 116) | class Baz < Foo
method boom_boom_room (line 117) | def boom_boom_room
method who (line 120) | def who(first, last)
class Fizzbuzz (line 125) | class Fizzbuzz < Baz
FILE: spec/functional/delay_spec.rb
type Functional (line 1) | module Functional
FILE: spec/functional/either_spec.rb
type Functional (line 3) | module Functional
FILE: spec/functional/final_struct_spec.rb
type Functional (line 3) | module Functional
function to_h (line 30) | def to_h; {answer: 42, harmless: 'mostly'}; end
function to_h (line 231) | def to_h; {answer: 42, harmless: 'mostly'}; end
FILE: spec/functional/final_var_spec.rb
type Functional (line 1) | module Functional
FILE: spec/functional/memo_spec.rb
type Functional (line 1) | module Functional
function create_new_memo_class (line 5) | def create_new_memo_class
function foo (line 67) | def self.foo
function foo (line 84) | def self.foo
function foo (line 101) | def self.foo
FILE: spec/functional/option_spec.rb
type Functional (line 4) | module Functional
FILE: spec/functional/pattern_matching_spec.rb
type Functional (line 3) | module Functional
function new_clazz (line 7) | def new_clazz(&block)
class Clazz (line 20) | class Clazz
class UnmatchedCallTesterSuperclass (line 87) | class UnmatchedCallTesterSuperclass
method foo (line 88) | def foo(bar)
class UnmatchedCallTesterSubclass (line 93) | class UnmatchedCallTesterSubclass < UnmatchedCallTesterSuperclass
class RecursiveCallTesterSuperclass (line 112) | class RecursiveCallTesterSuperclass
method foo (line 113) | def foo(bar)
class RecursiveCallTesterSubclass (line 118) | class RecursiveCallTesterSubclass < RecursiveCallTesterSuperclass
function tst (line 420) | def tst; :test end
FILE: spec/functional/protocol_info_spec.rb
type Functional (line 1) | module Functional
function bar (line 139) | def bar(a, b, c=1, d=2, *args); nil; end
function baz (line 140) | def self.baz(); nil; end
function bar (line 153) | def bar(); nil; end
function baz (line 154) | def self.baz(); nil; end
function bar (line 167) | def bar(a,b,c); nil; end
function baz (line 168) | def self.baz(a,b,c); nil; end
function bar (line 181) | def bar(a, b=1); nil; end
function baz (line 182) | def self.baz(a, b=1, c=2); nil; end
function bar (line 210) | def bar(a, *args); nil; end
function baz (line 211) | def self.baz(a, b, *args); nil; end
function bar (line 224) | def bar(*args); nil; end
function baz (line 225) | def self.baz(*args); nil; end
function foo (line 241) | def foo() true; end
function foo= (line 242) | def foo=(value) true; end
function foo (line 261) | def self.foo() true; end
function foo= (line 262) | def self.foo=(value) true; end
function foo (line 292) | def foo(); nil; end
function bar (line 293) | def bar(a, b, c); nil; end
function baz (line 294) | def baz(a, b, *args); nil; end
function foo (line 295) | def self.foo(); nil; end
function bar (line 296) | def self.bar(a, b, c); nil; end
function baz (line 297) | def self.baz(a, b, *args); nil; end
function foo (line 314) | def foo(*args); nil; end
function bar (line 315) | def bar(*args); nil; end
function baz (line 316) | def baz(*args); nil; end
function foo (line 317) | def self.foo(*args); nil; end
function bar (line 318) | def self.bar(*args); nil; end
function baz (line 319) | def self.baz(*args); nil; end
function bar (line 331) | def bar(a, b, *args); nil; end
function bar (line 343) | def self.bar(a, b, *args); nil; end
function foo (line 355) | def foo() true; end
function foo= (line 356) | def foo=() false; end
function foo (line 360) | def foo(value) false; end
function foo= (line 361) | def foo=(value) true; end
function foo (line 374) | def self.foo() true; end
function foo= (line 375) | def self.foo=() false; end
function foo (line 379) | def self.foo(value) false; end
function foo= (line 380) | def self.foo=(value) true; end
function instance_method (line 404) | def instance_method() 42; end
function class_method (line 410) | def class_method() 42; end
function instance_method (line 425) | def instance_method() 42; end
function class_method (line 431) | def class_method() 42; end
FILE: spec/functional/protocol_spec.rb
function foo (line 52) | def foo(); nil; end
function foo (line 78) | def foo(); nil; end
function foo (line 93) | def foo(); nil; end
function foo (line 108) | def foo(); nil; end
function bar (line 109) | def self.bar(); nil; end
function foo (line 124) | def foo(); nil; end
function bar (line 125) | def self.bar(); nil; end
function foo (line 142) | def foo(); nil; end
function foo (line 169) | def foo(); nil; end
function foo (line 193) | def foo(); nil; end
function bar (line 194) | def self.bar(); nil; end
function foo (line 209) | def foo(); nil; end
function bar (line 210) | def self.bar(); nil; end
FILE: spec/functional/record_spec.rb
type Functional (line 4) | module Functional
function foo (line 203) | def foo() nil end
function full_name (line 266) | def full_name
function formal_name (line 270) | def formal_name
FILE: spec/functional/tuple_spec.rb
type Functional (line 9) | module Functional
function to_a (line 34) | def to_a() [:foo, :bar, :baz]; end
function to_a (line 182) | def to_a() [2, 3, 4]; end
function to_a (line 244) | def to_a() [2, 3, 4]; end
function to_a (line 298) | def to_a() [4, 5, 6]; end
function to_a (line 368) | def to_a() [3, 4, 5]; end
function to_a (line 558) | def to_a() [1, 2, 3]; end
FILE: spec/functional/type_check_spec.rb
type Functional (line 1) | module Functional
FILE: spec/functional/union_spec.rb
type Functional (line 3) | module Functional
FILE: spec/functional/value_struct_spec.rb
type Functional (line 3) | module Functional
function each_pair (line 31) | def each_pair(&block)
function to_h (line 168) | def to_h; {answer: 42, harmless: 'mostly'}; end
Condensed preview — 59 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (288K chars).
[
{
"path": ".coveralls.yml",
"chars": 46,
"preview": "repo_token: M3JnILwxCIYb4OjWvyxBJkib9xsAGdnek\n"
},
{
"path": ".gitignore",
"chars": 339,
"preview": "Gemfile.lock\n.rspec-local\n*.gem\nlib/1.8\nlib/1.9\nlib/2.0\n.rvmrc\n.ruby-version\n.ruby-gemset\n.bundle/*\n.yardoc/*\nyardoc/*\nt"
},
{
"path": ".rspec",
"chars": 40,
"preview": "--require spec_helper\n--format progress\n"
},
{
"path": ".travis.yml",
"chars": 485,
"preview": "language: ruby\n\nrvm:\n - 2.2.3\n - 2.2.2\n - 2.2.1\n - 2.1.5\n - 2.1.4\n - 2.0.0\n - ruby-head\n - jruby-1.7.19\n - jrub"
},
{
"path": ".yardopts",
"chars": 171,
"preview": "--protected\n--no-private\n--embed-mixins\n--output-dir ./yardoc\n--markup markdown\n--title=Functional Ruby\n--template defau"
},
{
"path": "CHANGELOG.md",
"chars": 2373,
"preview": "## Current Release v1.3.0 (October 4, 2015)\n\n* Pattern match now check arity of pattern and block\n* `PatternMatching::AL"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 1981,
"preview": "# Contributor Code of Conduct\n\nAs contributors and maintainers of this project, and in the interest of fostering an open"
},
{
"path": "Gemfile",
"chars": 462,
"preview": "source 'https://rubygems.org'\n\ngemspec\n\ngroup :development do\n gem 'rake', '~> 12.3.0'\nend\n\ngroup :testing do\n gem 'rs"
},
{
"path": "LICENSE",
"chars": 1174,
"preview": "Copyright (c) Jerry D'Antonio -- released under the MIT license.\n\nhttp://www.opensource.org/licenses/mit-license.php \n\n"
},
{
"path": "README.md",
"chars": 7617,
"preview": "# Functional Ruby\n[](http://badge.fury.io/rb/functional-ruby"
},
{
"path": "Rakefile",
"chars": 543,
"preview": "$:.push File.join(File.dirname(__FILE__), 'lib')\n\nGEMSPEC = Gem::Specification.load('functional-ruby.gemspec')\n\nrequire "
},
{
"path": "appveyor.yml",
"chars": 460,
"preview": "install:\n - SET PATH=C:\\Ruby%ruby_version%\\bin;%PATH%\n - SET PATH=C:\\MinGW\\bin;%PATH%\n - SET RAKEOPT=-rdevkit\n - rub"
},
{
"path": "doc/memo.md",
"chars": 6851,
"preview": "# memoize\n\n### Rationale\n\n Many computational operations take a significant amount of time and/or use\n an inordin"
},
{
"path": "doc/memoize.rb",
"chars": 1489,
"preview": "#!/usr/bin/env ruby\n$LOAD_PATH << File.expand_path('../../lib', __FILE__)\n\nrequire 'functional'\n\nclass Factors\n include"
},
{
"path": "doc/pattern_matching.md",
"chars": 12291,
"preview": "### Features\n\n * Pattern matching for instance methods.\n * Pattern matching for object constructors.\n * Paramet"
},
{
"path": "doc/protocol.md",
"chars": 8491,
"preview": "### Rationale\n\n Traditional object orientation implements polymorphism inheritance. The *Is-A*\n relationship indi"
},
{
"path": "doc/record.md",
"chars": 11298,
"preview": "### Declaration\n\n A `Record` class is declared in a manner identical to that used with Ruby's `Struct`.\n The clas"
},
{
"path": "functional_ruby.gemspec",
"chars": 1050,
"preview": "$LOAD_PATH << File.expand_path('../lib', __FILE__)\n\nrequire 'functional/version'\n\nGem::Specification.new do |s|\n s.name"
},
{
"path": "lib/functional/abstract_struct.rb",
"chars": 4753,
"preview": "require 'functional/protocol'\nrequire 'functional/synchronization'\n\nFunctional::SpecifyProtocol(:Struct) do\n instance_m"
},
{
"path": "lib/functional/delay.rb",
"chars": 4075,
"preview": "require 'functional/synchronization'\n\nmodule Functional\n\n # Lazy evaluation of a block yielding an immutable result. Us"
},
{
"path": "lib/functional/either.rb",
"chars": 8721,
"preview": "require 'functional/abstract_struct'\nrequire 'functional/protocol'\nrequire 'functional/synchronization'\n\nFunctional::Spe"
},
{
"path": "lib/functional/final_struct.rb",
"chars": 8000,
"preview": "require 'functional/final_var'\nrequire 'functional/synchronization'\n\nmodule Functional\n\n # A variation on Ruby's `OpenS"
},
{
"path": "lib/functional/final_var.rb",
"chars": 5066,
"preview": "require 'functional/synchronization'\n\nmodule Functional\n\n # An exception raised when an attempt is made to modify an\n "
},
{
"path": "lib/functional/memo.rb",
"chars": 3692,
"preview": "require 'functional/synchronization'\n\nmodule Functional\n\n # Memoization is a technique for optimizing functions that ar"
},
{
"path": "lib/functional/method_signature.rb",
"chars": 2467,
"preview": "module Functional\n\n module PatternMatching\n\n # @!visibility private\n #\n # Helper functions used when pattern m"
},
{
"path": "lib/functional/option.rb",
"chars": 6850,
"preview": "require 'functional/abstract_struct'\nrequire 'functional/either'\nrequire 'functional/protocol'\nrequire 'functional/synch"
},
{
"path": "lib/functional/pattern_matching.rb",
"chars": 6045,
"preview": "require 'functional/method_signature'\n\nmodule Functional\n\n # As much as I love Ruby I've always been a little disappoin"
},
{
"path": "lib/functional/protocol.rb",
"chars": 6128,
"preview": "require 'functional/protocol_info'\n\nmodule Functional\n\n # An exception indicating a problem during protocol processing."
},
{
"path": "lib/functional/protocol_info.rb",
"chars": 6312,
"preview": "require 'functional/synchronization'\n\nmodule Functional\n\n # An immutable object describing a single protocol and capabl"
},
{
"path": "lib/functional/record.rb",
"chars": 7616,
"preview": "require 'functional/abstract_struct'\nrequire 'functional/protocol'\nrequire 'functional/type_check'\n\nmodule Functional\n\n "
},
{
"path": "lib/functional/synchronization.rb",
"chars": 2062,
"preview": "module Functional\n\n # @!visibility private\n #\n # Based on work originally done by Petr Chalupa (@pitr-ch) in Concurre"
},
{
"path": "lib/functional/tuple.rb",
"chars": 8772,
"preview": "require 'functional/synchronization'\n\nmodule Functional\n\n # A tuple is a pure functional data strcture that is similar "
},
{
"path": "lib/functional/type_check.rb",
"chars": 3888,
"preview": "module Functional\n\n # Supplies type-checking helpers whenever included.\n #\n # @see http://ruby-concurrency.github.io/"
},
{
"path": "lib/functional/union.rb",
"chars": 5781,
"preview": "require 'functional/abstract_struct'\nrequire 'functional/synchronization'\n\nmodule Functional\n\n # An immutable data stru"
},
{
"path": "lib/functional/value_struct.rb",
"chars": 4969,
"preview": "require 'functional/synchronization'\n\nmodule Functional\n\n # A variation on Ruby's `OpenStruct` in which all fields are "
},
{
"path": "lib/functional/version.rb",
"chars": 72,
"preview": "module Functional\n\n # The current gem version.\n VERSION = '1.3.0'\nend\n"
},
{
"path": "lib/functional.rb",
"chars": 846,
"preview": "require 'functional/delay'\nrequire 'functional/either'\nrequire 'functional/final_struct'\nrequire 'functional/final_var'\n"
},
{
"path": "spec/.gitignore",
"chars": 0,
"preview": ""
},
{
"path": "spec/functional/abstract_struct_shared.rb",
"chars": 4335,
"preview": "shared_examples :abstract_struct do\n\n specify { Functional::Protocol::Satisfy! struct_class, :Struct }\n\n let(:other_st"
},
{
"path": "spec/functional/complex_pattern_matching_spec.rb",
"chars": 5883,
"preview": "require 'ostruct'\n\nclass Bar\n def greet\n return 'Hello, World!'\n end\nend\n\nclass Foo < Bar\n include Functional::Pat"
},
{
"path": "spec/functional/delay_spec.rb",
"chars": 4045,
"preview": "module Functional\n\n describe Delay do\n\n let!(:fulfilled_value) { 10 }\n let!(:rejected_reason) { StandardError.new"
},
{
"path": "spec/functional/either_spec.rb",
"chars": 6914,
"preview": "require_relative 'abstract_struct_shared'\n\nmodule Functional\n\n describe Either do\n\n let!(:value){ 42 }\n let!(:rea"
},
{
"path": "spec/functional/final_struct_spec.rb",
"chars": 8426,
"preview": "require 'ostruct'\n\nmodule Functional\n\n describe FinalStruct do\n\n context 'instanciation' do\n\n specify 'with no "
},
{
"path": "spec/functional/final_var_spec.rb",
"chars": 4382,
"preview": "module Functional\n\n describe FinalVar do\n\n context 'instanciation' do\n\n it 'is unset when no arguments given' d"
},
{
"path": "spec/functional/memo_spec.rb",
"chars": 5159,
"preview": "module Functional\n\n describe Memo do\n\n def create_new_memo_class\n Class.new do\n include Functional::Memo"
},
{
"path": "spec/functional/option_spec.rb",
"chars": 8205,
"preview": "require_relative 'abstract_struct_shared'\nrequire 'securerandom'\n\nmodule Functional\n\n describe Option do\n\n let!(:val"
},
{
"path": "spec/functional/pattern_matching_spec.rb",
"chars": 12559,
"preview": "require 'ostruct'\n\nmodule Functional\n\n describe PatternMatching do\n\n def new_clazz(&block)\n clazz = Class.new\n "
},
{
"path": "spec/functional/protocol_info_spec.rb",
"chars": 11710,
"preview": "module Functional\n\n describe ProtocolInfo do\n\n let!(:kitchen_sink) do\n ProtocolInfo.new(:Everything) do\n "
},
{
"path": "spec/functional/protocol_spec.rb",
"chars": 7422,
"preview": "describe 'protocol specification' do\n\n before(:each) do\n @protocol_info = Functional::Protocol.class_variable_get(:@"
},
{
"path": "spec/functional/record_spec.rb",
"chars": 8419,
"preview": "require_relative 'abstract_struct_shared'\nrequire 'securerandom'\n\nmodule Functional\n\n describe Record do\n\n let!(:exp"
},
{
"path": "spec/functional/tuple_spec.rb",
"chars": 20759,
"preview": "require 'rspec/expectations'\n\nRSpec::Matchers.define :be_a_different_tuple_than do |expected|\n match do |actual|\n ac"
},
{
"path": "spec/functional/type_check_spec.rb",
"chars": 3043,
"preview": "module Functional\n\n describe TypeCheck do\n\n context 'Type?' do\n\n it 'returns true when value is of any of the t"
},
{
"path": "spec/functional/union_spec.rb",
"chars": 2968,
"preview": "require_relative 'abstract_struct_shared'\n\nmodule Functional\n\n describe Union do\n\n let!(:expected_fields){ [:a, :b, "
},
{
"path": "spec/functional/value_struct_spec.rb",
"chars": 6166,
"preview": "require 'ostruct'\n\nmodule Functional\n\n describe ValueStruct do\n\n context 'instanciation' do\n\n specify 'raises a"
},
{
"path": "spec/spec_helper.rb",
"chars": 675,
"preview": "require 'simplecov'\nrequire 'coveralls'\n\nSimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[\n SimpleCov::Format"
},
{
"path": "spec/support/.gitignore",
"chars": 0,
"preview": ""
},
{
"path": "tasks/.gitignore",
"chars": 0,
"preview": ""
},
{
"path": "tasks/metrics.rake",
"chars": 183,
"preview": "desc 'Display LOC (lines of code) report'\ntask :loc do\n sh 'countloc -r lib'\nend\n\ndesc 'Display code quality analysis r"
},
{
"path": "tasks/update_doc.rake",
"chars": 1176,
"preview": "require 'yard'\nYARD::Rake::YardocTask.new\n\nroot = File.expand_path File.join(File.dirname(__FILE__), '..')\n\nnamespace :y"
}
]
About this extraction
This page contains the full source code of the jdantonio/functional-ruby GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 59 files (269.2 KB), approximately 71.8k tokens, and a symbol index with 339 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.