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 [![Gem Version](https://badge.fury.io/rb/functional-ruby.svg)](http://badge.fury.io/rb/functional-ruby) [![Travis CI Build Status](https://secure.travis-ci.org/jdantonio/functional-ruby.png)](https://travis-ci.org/jdantonio/functional-ruby?branch=master) [![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/8xfy4a8lmc26112e/branch/master?svg=true)](https://ci.appveyor.com/project/jdantonio/functional-ruby/branch/master) [![Coverage Status](https://coveralls.io/repos/jdantonio/functional-ruby/badge.png)](https://coveralls.io/r/jdantonio/functional-ruby) [![Code Climate](https://codeclimate.com/github/jdantonio/functional-ruby.png)](https://codeclimate.com/github/jdantonio/functional-ruby) [![Inline docs](http://inch-ci.org/github/jdantonio/functional-ruby.png)](http://inch-ci.org/github/jdantonio/functional-ruby) [![Dependency Status](https://gemnasium.com/jdantonio/functional-ruby.png)](https://gemnasium.com/jdantonio/functional-ruby) [![License](http://img.shields.io/license/MIT.png?color=green)](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 #=> #"J.", :middle=>nil, :last=>"Doe", :suffix=>nil> matz = Name.new(first: 'Yukihiro', last: 'Matsumoto') #=> #"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') => "Dave", :address=>"123 Main"> Functional::Record::Customer.new(name: 'Dave', address: '123 Main') => "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') => 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) => "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) => "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') => "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 #=> # # # @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 # #=> # # f.set? #=> false # f.value #=> nil # f.value = 42 #=> 42 # f.inspect # #=> "#" # f.set? #=> true # f.value #=> 42 # # @example Instanciation With an Initial Value # f = Functional::FinalVar.new(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!') #=> # # 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') # #=> #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(/^#$/) 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 end specify '#some? returns false when none' do expect(none_subject).to_not be_some end specify '#none? returns true when none' do expect(none_subject).to be_none end specify '#none? returns false when the some value is set' do expect(some_subject).to_not be_none end specify '#some returns the some value when some is set' do expect(some_subject.some).to eq value end specify '#some returns nil when none is set' do expect(none_subject.some).to be_nil end it 'aliases #some? as #fulfilled?' do expect(some_subject).to be_fulfilled expect(none_subject).to_not be_fulfilled end it 'aliases #some? as #value?' do expect(some_subject).to be_value expect(none_subject).to_not be_value end it 'aliases #none? as #rejected?' do expect(some_subject).to_not be_rejected expect(none_subject).to be_rejected end it 'aliases #none? as #reason?' do expect(some_subject).to_not be_reason expect(none_subject).to be_reason end it 'aliases #some as #value' do expect(some_subject.value).to eq value expect(none_subject.value).to be_nil end specify '#reason returns nil when some' do expect(some_subject.reason).to be_nil end end context 'length' do it 'returns 1 when some' do expect(Option.some(:foo).length).to eq 1 end it 'returns 0 when none' do expect(Option.none.length).to eq 0 end it 'as aliased as #size' do expect(Option.some(:foo).size).to eq 1 expect(Option.none.size).to eq 0 end end context '#and' do it 'returns false when none' do expect(Option.none.and(true)).to be false end it 'returns true when some and other is a some Option' do other = Option.some(42) expect(Option.some(:foo).and(other)).to be true end it 'returns false when some and other is a none Option' do other = Option.none expect(Option.some(:foo).and(other)).to be false end it 'passes the value to the given block when some' do expected = false other = ->(some){ expected = some } Option.some(42).and(&other) expect(expected).to eq 42 end it 'returns true when some and the block returns a truthy value' do other = ->(some){ 'truthy' } expect(Option.some(42).and(&other)).to be true end it 'returns false when some and the block returns a falsey value' do other = ->(some){ nil } expect(Option.some(42).and(&other)).to be false end it 'returns true when some and given a truthy value' do expect(Option.some(42).and('truthy')).to be true end it 'returns false when some and given a falsey value' do expect(Option.some(42).and(nil)).to be false end it 'raises an exception when given both a value and a block' do expect { Option.some(42).and(:foo){|some| :bar } }.to raise_error(ArgumentError) end end context '#or' do it 'returns true when some' do expect(Option.some(42).or(nil)).to be true end it 'returns true when none and other is a some Option' do other = Option.some(42) expect(Option.none.or(other)).to be true end it 'returns false when none and other is a none Option' do other = Option.none expect(Option.none.or(other)).to be false end it 'returns true when none and the block returns a truthy value' do other = ->{ 42 } expect(Option.none.or(&other)).to be true end it 'returns false when none and the block returns a falsey value' do other = ->{ false } expect(Option.none.or(&other)).to be false end it 'returns true when none and given a truthy value' do expect(Option.none.or('truthy')).to be true end it 'returns false when none and given a falsey value' do expect(Option.none.or(nil)).to be false end it 'raises an exception when given both a value and a block' do expect { Option.none.and(:foo){ :bar } }.to raise_error(ArgumentError) end end context '#else' do it 'returns the value when some' do expect(Option.some(some_value).else(other_value)).to eq some_value end it 'returns the given value when none' do expect(Option.none.else(other_value)).to eq other_value end it 'returns the other value when none and given a some Option' do other = Option.some(other_value) expect(Option.none.else(other)).to eq other_value end it 'returns nil when none and given a none Option' do other = Option.none expect(Option.none.else(other)).to be_nil end it 'returns the result of the given block when none' do other = ->{ other_value } expect(Option.none.else(&other)).to eq other_value end it 'raises an exception when given both a value and a block' do expect { Option.none.else(:foo){ :bar } }.to raise_error(ArgumentError) end end context '#iff' do it 'returns a some option with the given value when the boolean is true' do subject = Option.iff(:foo, true) expect(subject).to be_some expect(subject.some).to eq :foo end it 'returns a none option when the boolean is false' do subject = Option.iff(:foo, false) expect(subject).to be_none expect(subject.some).to be_nil end it 'returns a some option with the given value when the block is truthy' do subject = Option.iff(:foo){ :baz } expect(subject).to be_some expect(subject.some).to eq :foo end it 'returns a none option when the block is false' do subject = Option.iff(:foo){ false } expect(subject).to be_none expect(subject.some).to be_nil end it 'returns a none option when the block is nil' do subject = Option.iff(:foo){ nil } expect(subject).to be_none expect(subject.some).to be_nil end it 'raises an exception when both a boolean and a block are given' do expect { subject = Option.iff(:foo, true){ nil } }.to raise_error(ArgumentError) end end end end ================================================ FILE: spec/functional/pattern_matching_spec.rb ================================================ require 'ostruct' module Functional describe PatternMatching do def new_clazz(&block) clazz = Class.new clazz.send(:include, PatternMatching) clazz.instance_eval(&block) if block_given? return clazz end subject { new_clazz } context '#defn declaration' do it 'can be used within a class declaration' do expect { class Clazz include PatternMatching defn(:foo){} end }.not_to raise_error end it 'can be used on a class object' do expect { clazz = Class.new clazz.send(:include, PatternMatching) clazz.defn(:foo){} }.not_to raise_error end it 'requires a block' do expect { clazz = Class.new clazz.send(:include, PatternMatching) clazz.defn(:foo) }.to raise_error(ArgumentError) end end context 'constructor' do it 'can pattern match the constructor' do unless RUBY_VERSION == '1.9.2' subject.defn(:initialize, PatternMatching::UNBOUND, PatternMatching::UNBOUND, PatternMatching::UNBOUND) { |_,_,_| 'three args' } subject.defn(:initialize, PatternMatching::UNBOUND, PatternMatching::UNBOUND) { |_,_| 'two args' } subject.defn(:initialize, PatternMatching::UNBOUND) { |_| 'one arg' } expect { subject.new(1) }.not_to raise_error expect { subject.new(1, 2) }.not_to raise_error expect { subject.new(1, 2, 3) }.not_to raise_error expect { subject.new(1, 2, 3, 4) }.to raise_error end end end context 'parameter count' do it 'does not match a call with not enough arguments' do subject.defn(:foo, true) { 'expected' } expect { subject.new.foo() }.to raise_error(NoMethodError) end it 'does not match a call with too many arguments' do subject.defn(:foo, true) { 'expected' } expect { subject.new.foo(true, false) }.to raise_error(NoMethodError) end end context 'recursion' do it 'defers unmatched calls to the superclass' do class UnmatchedCallTesterSuperclass def foo(bar) return bar end end class UnmatchedCallTesterSubclass < UnmatchedCallTesterSuperclass include PatternMatching defn(:foo) { 'baz' } end subject = UnmatchedCallTesterSubclass.new expect(subject.foo(:bar)).to eq :bar end it 'can call another match from within a match' do subject.defn(:foo, :bar) { foo(:baz) } subject.defn(:foo, :baz) { 'expected' } expect(subject.new.foo(:bar)).to eq 'expected' end it 'can call a superclass method from within a match' do class RecursiveCallTesterSuperclass def foo(bar) return bar end end class RecursiveCallTesterSubclass < RecursiveCallTesterSuperclass include PatternMatching defn(:foo, :bar) { foo(:baz) } end subject = RecursiveCallTesterSubclass.new expect(subject.foo(:bar)).to eq :baz end end context 'datatypes' do it 'matches an argument of the class given in the match parameter' do subject.defn(:foo, Integer) { |_| 'expected' } expect(subject.new.foo(100)).to eq 'expected' expect { subject.new.foo('hello') }.to raise_error(NoMethodError) end it 'passes the matched argument to the block' do subject.defn(:foo, Integer) { |arg| arg } expect(subject.new.foo(100)).to eq 100 end end context 'function with no parameters' do it 'accepts no parameters' do subject.defn(:foo){} obj = subject.new expect { obj.foo }.not_to raise_error end it 'does not accept any parameters' do subject.defn(:foo){} obj = subject.new expect { obj.foo(1) }.to raise_error(NoMethodError) end it 'returns the correct value' do subject.defn(:foo){ true } expect(subject.new.foo).to be true end end context 'function with one parameter' do it 'matches a nil parameter' do subject.defn(:foo, nil) { 'expected' } expect(subject.new.foo(nil)).to eq 'expected' expect { subject.new.foo('no match should be found') }.to raise_error(NoMethodError) end it 'matches a boolean parameter' do subject.defn(:foo, true) { 'expected' } subject.defn(:foo, false) { 'false case' } expect(subject.new.foo(true)).to eq 'expected' expect(subject.new.foo(false)).to eq 'false case' expect { subject.new.foo('no match should be found') }.to raise_error(NoMethodError) end it 'matches a symbol parameter' do subject.defn(:foo, :bar) { 'expected' } expect(subject.new.foo(:bar)).to eq 'expected' expect { subject.new.foo(:baz) }.to raise_error(NoMethodError) end it 'matches a number parameter' do subject.defn(:foo, 10) { 'expected' } expect(subject.new.foo(10)).to eq 'expected' expect { subject.new.foo(11.0) }.to raise_error(NoMethodError) end it 'matches a string parameter' do subject.defn(:foo, 'bar') { 'expected' } expect(subject.new.foo('bar')).to eq 'expected' expect { subject.new.foo('baz') }.to raise_error(NoMethodError) end it 'matches an array parameter' do subject.defn(:foo, [1, 2, 3]) { 'expected' } expect(subject.new.foo([1, 2, 3])).to eq 'expected' expect { subject.new.foo([3, 4, 5]) }.to raise_error(NoMethodError) end it 'matches a hash parameter' do subject.defn(:foo, bar: 1, baz: 2) { |_| 'expected' } expect(subject.new.foo(bar: 1, baz: 2)).to eq 'expected' expect { subject.new.foo(foo: 0, bar: 1) }.to raise_error(NoMethodError) end it 'matches an object parameter' do subject.defn(:foo, OpenStruct.new(foo: :bar)) { 'expected' } expect(subject.new.foo(OpenStruct.new(foo: :bar))).to eq 'expected' expect { subject.new.foo(OpenStruct.new(bar: :baz)) }.to raise_error(NoMethodError) end it 'matches an unbound parameter' do subject.defn(:foo, PatternMatching::UNBOUND) {|arg| arg } expect(subject.new.foo(:foo)).to eq :foo end end context 'function with two parameters' do it 'matches two bound arguments' do subject.defn(:foo, :male, :female){ 'expected' } expect(subject.new.foo(:male, :female)).to eq 'expected' expect { subject.new.foo(1, 2) }.to raise_error(NoMethodError) end it 'matches two unbound arguments' do subject.defn(:foo, PatternMatching::UNBOUND, PatternMatching::UNBOUND) do |first, second| [first, second] end expect(subject.new.foo(:male, :female)).to eq [:male, :female] end it 'matches when the first argument is bound and the second is not' do subject.defn(:foo, :male, PatternMatching::UNBOUND) do |second| second end expect(subject.new.foo(:male, :female)).to eq :female end it 'matches when the second argument is bound and the first is not' do subject.defn(:foo, PatternMatching::UNBOUND, :female) do |first| first end expect(subject.new.foo(:male, :female)).to eq :male end end context 'functions with hash arguments' do it 'matches an empty argument hash with an empty parameter hash' do subject.defn(:foo, {}) { |_| true } expect(subject.new.foo({})).to be true expect { subject.new.foo({one: :two}) }.to raise_error(NoMethodError) end it 'matches when all hash keys and values match' do subject.defn(:foo, {bar: :baz}) { |_| true } expect(subject.new.foo(bar: :baz)).to be true expect { subject.new.foo({one: :two}) }.to raise_error(NoMethodError) end it 'matches when every pattern key/value are in the argument' do subject.defn(:foo, {bar: :baz}) { |_| true } expect(subject.new.foo(foo: :bar, bar: :baz)).to be true end it 'matches when all keys with unbound values in the pattern have an argument' do subject.defn(:foo, {bar: PatternMatching::UNBOUND}) { |_| true } expect(subject.new.foo(bar: :baz)).to be true end it 'passes unbound values to the block' do subject.defn(:foo, {bar: PatternMatching::UNBOUND}) {|arg| arg } expect(subject.new.foo(bar: :baz)).to eq :baz end it 'passes the matched hash to the block' do subject.defn(:foo, {bar: :baz}) { |opts| opts } expect(subject.new.foo(bar: :baz)).to eq({bar: :baz}) end it 'does not match a non-hash argument' do subject.defn(:foo, {}) { |_| true } expect { subject.new.foo(:bar) }.to raise_error(NoMethodError) end it 'supports idiomatic has-as-last-argument syntax' do subject.defn(:foo, PatternMatching::UNBOUND) { |opts| opts } expect(subject.new.foo(bar: :baz, one: 1, many: 2)).to eq({bar: :baz, one: 1, many: 2}) end end context 'varaible-length argument lists' do it 'supports ALL as the last parameter' do subject.defn(:foo, 1, 2, PatternMatching::ALL) { |*args| args } expect(subject.new.foo(1, 2, 3)).to eq([3]) expect(subject.new.foo(1, 2, :foo, :bar)).to eq([:foo, :bar]) expect(subject.new.foo(1, 2, :foo, :bar, one: 1, two: 2)).to eq([:foo, :bar, {one: 1, two: 2}]) end end context 'guard clauses' do it 'matches when the guard clause returns true' do subject.defn(:old_enough, PatternMatching::UNBOUND){ |_| true }.when{|x| x > 16 } expect(subject.new.old_enough(20)).to be true end it 'does not match when the guard clause returns false' do subject.defn(:old_enough, PatternMatching::UNBOUND){ |_| true }.when{|x| x > 16 } expect { subject.new.old_enough(10) }.to raise_error(NoMethodError) end it 'continues pattern matching when the guard clause returns false' do subject.defn(:old_enough, PatternMatching::UNBOUND){ |_| true }.when{|x| x > 16 } subject.defn(:old_enough, PatternMatching::UNBOUND) { |_| false } expect(subject.new.old_enough(10)).to be false end it 'raises an exception when the guard clause does not have a block' do expect { subject.defn(:initialize, PatternMatching::UNBOUND) { 'one arg' }.when }.to raise_error(ArgumentError) end end context "NoMethodError" do let (:parent) do Class.new { include PatternMatching; def tst; :test end } end let (:child) { Class.new(parent) } # let (:child_inst) { child.new } it "throws if pattern don't match and no super" do child.defn(:no_method) { } expect { child.new.no_method(1) }.to raise_error(NoMethodError) end it "calls super if pattern don't match and there is super" do child.defn(:tst, PatternMatching::UNBOUND) { |_| } expect(child.new.tst).to eq(:test) end it "throws if it raised inside method body" do child.defn(:raiser) { raise NoMethodError, "no_method" } expect { child.new.raiser }.to raise_error(NoMethodError, "no_method") end end context "ArgumentError" do it "throws error if block and pattern args missmatch" do expect do subject.defn(:raise) { |_| } end.to raise_error(ArgumentError) expect do subject.defn(:raise, PatternMatching::UNBOUND) { } end.to raise_error(ArgumentError) expect do subject.defn(:raise, PatternMatching::ANY) { } end expect do subject.defn(:imok, PatternMatching::UNBOUND, { k: _}, PatternMatching::ANY) { |_, _, *args| } end end end end end ================================================ FILE: spec/functional/protocol_info_spec.rb ================================================ module Functional describe ProtocolInfo do let!(:kitchen_sink) do ProtocolInfo.new(:Everything) 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 end context '#initialize' do it 'raises an exception when no block is given' do expect { ProtocolInfo.new(:Foo) }.to raise_error(ArgumentError) end it 'raises an exception when the name is nil' do expect { ProtocolInfo.new(nil){ nil } }.to raise_error(ArgumentError) end it 'raises an exception when the name is blank' do expect { ProtocolInfo.new(''){ nil } }.to raise_error(ArgumentError) end it 'specifies an instance method with no arity given' do info = ProtocolInfo.new(:Foo) do instance_method :foo end expect(info.instance_methods[:foo]).to be_nil end it 'specifies an instance method with a given arity' do info = ProtocolInfo.new(:Foo) do instance_method :foo, 2 end expect(info.instance_methods[:foo]).to eq 2 end it 'specifies a class method with any arity' do info = ProtocolInfo.new(:Foo) do class_method :foo end expect(info.class_methods[:foo]).to be_nil end it 'specifies a class method with a given arity' do info = ProtocolInfo.new(:Foo) do class_method :foo, 2 end expect(info.class_methods[:foo]).to eq 2 end it 'specifies an instance attribute reader' do info = ProtocolInfo.new(:Foo) do attr_reader :foo end expect(info.instance_methods[:foo]).to eq 0 end it 'specifies an instance attribute writer' do info = ProtocolInfo.new(:Foo) do attr_writer :foo end expect(info.instance_methods[:foo=]).to eq 1 end it 'specifies an instance attribute accessor' do info = ProtocolInfo.new(:Foo) do attr_accessor :foo end expect(info.instance_methods[:foo]).to eq 0 expect(info.instance_methods[:foo=]).to eq 1 end it 'specifies a class attribute reader' do info = ProtocolInfo.new(:Foo) do class_attr_reader :foo end expect(info.class_methods[:foo]).to eq 0 end it 'specifies a class attribute writer' do info = ProtocolInfo.new(:Foo) do class_attr_writer :foo end expect(info.class_methods[:foo=]).to eq 1 end it 'specifies a class attribute accessor' do info = ProtocolInfo.new(:Foo) do class_attr_accessor :foo end expect(info.class_methods[:foo]).to eq 0 expect(info.class_methods[:foo=]).to eq 1 end it 'specifies a constant' do info = ProtocolInfo.new(:Foo) do constant :FOO end expect(info.constants).to include :FOO end end context '#satisfies?' do it 'validates methods with no arity given' do info = ProtocolInfo.new(:Foo) do instance_method(:bar) class_method(:baz) end clazz = Class.new do def bar(a, b, c=1, d=2, *args); nil; end def self.baz(); nil; end end expect(info.satisfies?(clazz.new)).to be true end it 'validates methods with no parameters' do info = ProtocolInfo.new(:Foo) do instance_method(:bar, 0) class_method(:baz, 0) end clazz = Class.new do def bar(); nil; end def self.baz(); nil; end end expect(info.satisfies?(clazz.new)).to be true end it 'validates methods with a fixed number of parameters' do info = ProtocolInfo.new(:Foo) do instance_method(:bar, 3) class_method(:baz, 3) end clazz = Class.new do def bar(a,b,c); nil; end def self.baz(a,b,c); nil; end end expect(info.satisfies?(clazz.new)).to be true end it 'validates methods with optional parameters' do info = ProtocolInfo.new(:Foo) do instance_method(:bar, -2) class_method(:baz, -2) end clazz = Class.new do def bar(a, b=1); nil; end def self.baz(a, b=1, c=2); nil; end end expect(info.satisfies?(clazz.new)).to be true end ##NOTE: Syntax error on JRuby and Rbx #it 'validates methods with keyword parameters' do # info = ProtocolInfo.new(:Foo) do # instance_method(:bar, -2) # class_method(:baz, -3) # end # # clazz = Class.new do # def bar(a, foo: 'foo', baz: 'baz'); nil; end # def self.baz(a, b, foo: 'foo', baz: 'baz'); nil; end # end # # expect(info.satisfies?(clazz.new)).to be true #end it 'validates methods with variable length argument lists' do info = ProtocolInfo.new(:Foo) do instance_method(:bar, -2) class_method(:baz, -3) end clazz = Class.new do def bar(a, *args); nil; end def self.baz(a, b, *args); nil; end end expect(info.satisfies?(clazz.new)).to be true end it 'validates methods with arity -1' do info = ProtocolInfo.new(:Foo) do instance_method(:bar, -1) class_method(:baz, -1) end clazz = Class.new do def bar(*args); nil; end def self.baz(*args); nil; end end expect(info.satisfies?(clazz.new)).to be true end it 'validates instance attribute accessors' do info = ProtocolInfo.new(:Foo) do attr_accessor :foo end accessor_clazz = Class.new do attr_accessor :foo end manual_clazz = Class.new do def foo() true; end def foo=(value) true; end end expect(info.satisfies?(accessor_clazz.new)).to be true expect(info.satisfies?(manual_clazz.new)).to be true end it 'validates class attribute accessors' do info = ProtocolInfo.new(:Foo) do class_attr_accessor :foo end accessor_clazz = Class.new do class << self attr_accessor :foo end end manual_clazz = Class.new do def self.foo() true; end def self.foo=(value) true; end end expect(info.satisfies?(accessor_clazz.new)).to be true expect(info.satisfies?(manual_clazz.new)).to be true end it 'validates constants' do info = ProtocolInfo.new(:Foo) do constant :FOO end clazz = Class.new do FOO = 42 end expect(info.satisfies?(clazz.new)).to be false end it 'always accepts methods when arity not given' do info = ProtocolInfo.new(:Foo) do instance_method(:foo) instance_method(:bar) instance_method(:baz) class_method(:foo) class_method(:bar) class_method(:baz) end clazz = Class.new do def foo(); nil; end def bar(a, b, c); nil; end def baz(a, b, *args); nil; end def self.foo(); nil; end def self.bar(a, b, c); nil; end def self.baz(a, b, *args); nil; end end expect(info.satisfies?(clazz.new)).to be true end it 'always accepts methods with arity -1' do info = ProtocolInfo.new(:Foo) do instance_method(:foo, 0) instance_method(:bar, 2) instance_method(:baz, -2) class_method(:foo, 0) class_method(:bar, -2) class_method(:baz, 2) end clazz = Class.new do def foo(*args); nil; end def bar(*args); nil; end def baz(*args); nil; end def self.foo(*args); nil; end def self.bar(*args); nil; end def self.baz(*args); nil; end end expect(info.satisfies?(clazz.new)).to be true end it 'returns false if one or more instance methods do not match' do info = ProtocolInfo.new(:Foo) do instance_method(:bar, 0) end clazz = Class.new do def bar(a, b, *args); nil; end end expect(info.satisfies?(clazz.new)).to be false end it 'returns false if one or more class methods do not match' do info = ProtocolInfo.new(:Foo) do class_method(:bar, 0) end clazz = Class.new do def self.bar(a, b, *args); nil; end end expect(info.satisfies?(clazz.new)).to be false end it 'returns false if one or more instance attributes does not match' do info = ProtocolInfo.new(:Foo) do attr_accessor :foo end reader_clazz = Class.new do def foo() true; end def foo=() false; end end writer_clazz = Class.new do def foo(value) false; end def foo=(value) true; end end expect(info.satisfies?(reader_clazz.new)).to be false expect(info.satisfies?(writer_clazz.new)).to be false end it 'returns false if one or more class attributes does not match' do info = ProtocolInfo.new(:Foo) do class_attr_accessor :foo end reader_clazz = Class.new do def self.foo() true; end def self.foo=() false; end end writer_clazz = Class.new do def self.foo(value) false; end def self.foo=(value) true; end end expect(info.satisfies?(reader_clazz.new)).to be false expect(info.satisfies?(writer_clazz.new)).to be false end it 'returns false if one or more constants has not been defined' do info = ProtocolInfo.new(:Foo) do constant :FOO end clazz = Class.new do BAR = 42 end expect(info.satisfies?(clazz.new)).to be false end it 'supports all specifiable characteristics on classes' do clazz = Class.new do attr_accessor :attr_accessor attr_reader :attr_reader attr_writer :attr_writer def instance_method() 42; end class << self attr_accessor :class_attr_accessor attr_reader :class_attr_reader attr_writer :class_attr_writer def class_method() 42; end end end clazz.const_set(:CONSTANT, 42) expect( kitchen_sink.satisfies?(clazz) ).to be true end it 'supports all specifiable characteristics on modules' do mod = Module.new do attr_accessor :attr_accessor attr_reader :attr_reader attr_writer :attr_writer def instance_method() 42; end class << self attr_accessor :class_attr_accessor attr_reader :class_attr_reader attr_writer :class_attr_writer def class_method() 42; end end end mod.const_set(:CONSTANT, 42) expect( kitchen_sink.satisfies?(mod) ).to be true end end end end ================================================ FILE: spec/functional/protocol_spec.rb ================================================ describe 'protocol specification' do before(:each) do @protocol_info = Functional::Protocol.class_variable_get(:@@info) Functional::Protocol.class_variable_set(:@@info, {}) end after(:each) do Functional::Protocol.class_variable_set(:@@info, @protocol_info) end context 'SpecifyProtocol method' do context 'without a block' do it 'returns the specified protocol when defined' do Functional::SpecifyProtocol(:Foo){ nil } expect(Functional::SpecifyProtocol(:Foo)).to_not be_nil end it 'returns nil when not defined' do expect(Functional::SpecifyProtocol(:Foo)).to be_nil end end context 'with a block' do it 'raises an exception if the protocol has already been specified' do Functional::SpecifyProtocol(:Foo){ nil } expect { Functional::SpecifyProtocol(:Foo){ nil } }.to raise_error(Functional::ProtocolError) end it 'returns the specified protocol once defined' do expect(Functional::SpecifyProtocol(:Foo){ nil }).to be_a Functional::ProtocolInfo end end end describe Functional::Protocol do context 'Satisfy?' do it 'accepts and checks multiple protocols' do Functional::SpecifyProtocol(:foo){ instance_method(:foo) } Functional::SpecifyProtocol(:bar){ instance_method(:foo) } Functional::SpecifyProtocol(:baz){ instance_method(:foo) } clazz = Class.new do def foo(); nil; end end expect( Functional::Protocol.Satisfy?(clazz.new, :foo, :bar, :baz) ).to be true end it 'returns false if one or more protocols have not been defined' do Functional::SpecifyProtocol(:foo){ instance_method(:foo) } expect( Functional::Protocol.Satisfy?('object', :foo, :bar) ).to be false end it 'raises an exception if no protocols are listed' do expect { Functional::Protocol::Satisfy?('object') }.to raise_error(ArgumentError) end it 'returns true on success' do Functional::SpecifyProtocol(:foo){ instance_method(:foo) } clazz = Class.new do def foo(); nil; end end expect( Functional::Protocol.Satisfy?(clazz.new, :foo) ).to be true end it 'returns false on failure' do Functional::SpecifyProtocol(:foo) do instance_method(:foo, 0) class_method(:bar, 0) end clazz = Class.new do def foo(); nil; end end expect( Functional::Protocol.Satisfy?(clazz.new, :foo) ).to be false end it 'validates classes' do Functional::SpecifyProtocol(:foo) do instance_method(:foo) class_method(:bar) end clazz = Class.new do def foo(); nil; end def self.bar(); nil; end end expect( Functional::Protocol.Satisfy?(clazz, :foo) ).to be true end it 'validates modules' do Functional::SpecifyProtocol(:foo) do instance_method(:foo) class_method(:bar) end mod = Module.new do def foo(); nil; end def self.bar(); nil; end end expect( Functional::Protocol.Satisfy?(mod, :foo) ).to be true end end context 'Satisfy!' do it 'accepts and checks multiple protocols' do Functional::SpecifyProtocol(:foo){ instance_method(:foo) } Functional::SpecifyProtocol(:bar){ instance_method(:foo) } Functional::SpecifyProtocol(:baz){ instance_method(:foo) } clazz = Class.new do def foo(); nil; end end target = clazz.new expect( Functional::Protocol.Satisfy!(target, :foo, :bar, :baz) ).to eq target end it 'raises an exception if one or more protocols have not been defined' do Functional::SpecifyProtocol(:foo){ instance_method(:foo) } expect{ Functional::Protocol.Satisfy!('object', :foo, :bar) }.to raise_error(Functional::ProtocolError) end it 'raises an exception if no protocols are listed' do expect { Functional::Protocol::Satisfy!('object') }.to raise_error(ArgumentError) end it 'returns the target on success' do Functional::SpecifyProtocol(:foo){ instance_method(:foo) } clazz = Class.new do def foo(); nil; end end target = clazz.new expect( Functional::Protocol.Satisfy!(target, :foo) ).to eq target end it 'raises an exception on failure' do Functional::SpecifyProtocol(:foo){ instance_method(:foo) } expect{ Functional::Protocol.Satisfy!('object', :foo) }.to raise_error(Functional::ProtocolError) end it 'validates classes' do Functional::SpecifyProtocol(:foo) do instance_method(:foo) class_method(:bar) end clazz = Class.new do def foo(); nil; end def self.bar(); nil; end end expect{ Functional::Protocol.Satisfy!(clazz, :foo) }.to_not raise_exception end it 'validates modules' do Functional::SpecifyProtocol(:foo) do instance_method(:foo) class_method(:bar) end mod = Module.new do def foo(); nil; end def self.bar(); nil; end end expect{ Functional::Protocol.Satisfy!(mod, :foo) }.to_not raise_exception end end context 'Specified?' do it 'returns true when all protocols have been defined' do Functional::SpecifyProtocol(:foo){ nil } Functional::SpecifyProtocol(:bar){ nil } Functional::SpecifyProtocol(:baz){ nil } expect(Functional::Protocol.Specified?(:foo, :bar, :baz)).to be true end it 'returns false when one or more of the protocols have not been defined' do Functional::SpecifyProtocol(:foo){ nil } Functional::SpecifyProtocol(:bar){ nil } expect(Functional::Protocol.Specified?(:foo, :bar, :baz)).to be false end it 'raises an exception when no protocols are given' do expect { Functional::Protocol.Specified? }.to raise_error(ArgumentError) end end context 'Specified!' do it 'returns true when all protocols have been defined' do Functional::SpecifyProtocol(:foo){ nil } Functional::SpecifyProtocol(:bar){ nil } Functional::SpecifyProtocol(:baz){ nil } expect(Functional::Protocol.Specified!(:foo, :bar, :baz)).to be true expect { Functional::Protocol.Specified!(:foo, :bar, :baz) }.to_not raise_error end it 'raises an exception when one or more of the protocols have not been defined' do Functional::SpecifyProtocol(:foo){ nil } Functional::SpecifyProtocol(:bar){ nil } expect { Functional::Protocol.Specified!(:foo, :bar, :baz) }.to raise_error(Functional::ProtocolError) end it 'raises an exception when no protocols are given' do expect { Functional::Protocol.Specified! }.to raise_error(ArgumentError) end end end end ================================================ FILE: spec/functional/record_spec.rb ================================================ require_relative 'abstract_struct_shared' require 'securerandom' module Functional describe Record do let!(:expected_fields){ [:a, :b, :c] } let!(:expected_values){ [42, nil, nil] } let(:struct_class) { Record.new(*expected_fields) } let(:struct_object) { struct_class.new(struct_class.fields.first => 42) } let(:other_object) { struct_class.new(struct_class.fields.first => Object.new) } it_should_behave_like :abstract_struct context 'definition' do it 'does not register a new class when no name is given' do Record.new(:foo, :bar, :baz) expect(defined?(Record::Foo)).to be_falsey end it 'creates a new class when given an array of field names' do clazz = Record.new(:foo, :bar, :baz) expect(clazz).to be_a Class expect(clazz.ancestors).to include(Functional::AbstractStruct) end it 'registers the new class with Record when given a string name and an array' do Record.new('Bar', :foo, :bar, :baz) expect(defined?(Record::Bar)).to eq 'constant' end it 'creates a new class when given a hash of field names and types/protocols' do clazz = Record.new(foo: String, bar: String, baz: String) expect(clazz).to be_a Class expect(clazz.ancestors).to include(Functional::AbstractStruct) end it 'registers the new class with Record when given a string name and a hash' do Record.new('Boom', foo: String, bar: String, baz: String) expect(defined?(Record::Boom)).to eq 'constant' end it 'raises an exception when given a hash with an invalid type/protocol' do expect { Record.new(foo: 'String', bar: String, baz: String) }.to raise_error(ArgumentError) end it 'raises an exception when given an invalid definition' do expect { Record.new(:foo, bar: String, baz: String) }.to raise_error(ArgumentError) end end context 'initialization' do it 'sets all fields values to nil' do fields = [:foo, :bar, :baz] clazz = Record.new(*fields) record = clazz.new fields.each do |field| expect(record.send(field)).to be_nil end end it 'sets initial values based on values given at object construction' do clazz = Record.new(:foo, :bar, :baz) record = clazz.new(foo: 1, bar: 2, baz: 3) expect(record.foo).to eq 1 expect(record.bar).to eq 2 expect(record.baz).to eq 3 end context 'with default values' do it 'defaults fields to values given during class creation' do clazz = Record.new(:foo, :bar, :baz) do default :foo, 42 default :bar, 'w00t!' end record = clazz.new expect(record.foo).to eq 42 expect(record.bar).to eq 'w00t!' expect(record.baz).to be_nil end it 'overrides default values with values provided at object construction' do clazz = Record.new(:foo, :bar, :baz) do default :foo, 42 default :bar, 'w00t!' default :baz, :bogus end record = clazz.new(foo: 1, bar: 2) expect(record.foo).to eq 1 expect(record.bar).to eq 2 expect(record.baz).to eq :bogus end it 'duplicates default values when assigning to a new object' do original = 'Foo' clazz = Record.new(:foo, :bar, :baz) do default :foo, original end record = clazz.new expect(record.foo).to eq original expect(record.foo.object_id).to_not eql original.object_id end it 'does not conflate defaults across record classes' do clazz_foo = Record.new(:foo, :bar, :baz) do default :foo, 42 end clazz_matz = Record.new(:foo, :bar, :baz) do default :foo, 'Matsumoto' end expect(clazz_foo.new.foo).to eq 42 expect(clazz_matz.new.foo).to eq 'Matsumoto' end end context 'with mandatory fields' do it 'raises an exception when values for requred field are not provided' do clazz = Record.new(:foo, :bar, :baz) do mandatory :foo end expect { clazz.new(bar: 1) }.to raise_exception(ArgumentError) end it 'raises an exception when required values are nil' do clazz = Record.new(:foo, :bar, :baz) do mandatory :foo end expect { clazz.new(foo: nil, bar: 1) }.to raise_exception(ArgumentError) end it 'allows multiple required fields to be specified together' do clazz = Record.new(:foo, :bar, :baz) do mandatory :foo, :bar, :baz end expect { clazz.new(foo: 1, bar: 2) }.to raise_exception(ArgumentError) expect { clazz.new(bar: 2, baz: 3) }.to raise_exception(ArgumentError) expect { clazz.new(foo: 1, bar: 2, baz: 3) }.to_not raise_exception end it 'does not conflate default values across record classes' do clazz_foo = Record.new(:foo, :bar, :baz) do mandatory :foo end clazz_baz = Record.new(:foo, :bar, :baz) do mandatory :baz end expect { clazz_foo.new(foo: 42) }.to_not raise_error expect { clazz_baz.new(baz: 42) }.to_not raise_error end end context 'with field type specification' do let(:type_safe_definition) do {foo: String, bar: Fixnum, baz: protocol} end let(:protocol){ SecureRandom.uuid.to_sym } let(:clazz_with_protocol) do Class.new do def foo() nil end end end let(:record_clazz) do Record.new(type_safe_definition) end before(:each) do Functional::SpecifyProtocol(protocol){ instance_method(:foo) } end it 'raises an exception for a value with an invalid type' do expect { record_clazz.new(foo: 'foo', bar: 'bar', baz: clazz_with_protocol.new) }.to raise_error(ArgumentError) end it 'raises an exception for a value that does not satisfy a protocol' do expect { record_clazz.new(foo: 'foo', bar: 42, baz: 'baz') }.to raise_error(ArgumentError) end it 'creates the object when all values match the appropriate types and protocols' do record = record_clazz.new(foo: 'foo', bar: 42, baz: clazz_with_protocol.new) expect(record).to be_a record_clazz end end it 'allows a field to be required and have a default value' do clazz = Record.new(:foo, :bar, :baz) do mandatory :foo default :foo, 42 end expect { clazz.new }.to_not raise_exception expect(clazz.new.foo).to eq 42 end it 'raises an exception if the default value for a require field is nil' do clazz = Record.new(:foo, :bar, :baz) do mandatory :foo default :foo, nil end expect { clazz.new }.to raise_exception(ArgumentError) end end context 'subclassing' do specify 'supports all capabilities on subclasses' do record_clazz = Functional::Record.new(:first, :middle, :last, :suffix) do mandatory :first, :last end clazz = Class.new(record_clazz) do 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 = clazz.new(first: 'Jerry', last: "D'Antonio") ted = clazz.new(first: 'Ted', middle: 'Theodore', last: 'Logan', suffix: 'Esq.') expect(jerry.full_name).to eq "Jerry D'Antonio" expect(jerry.formal_name).to eq "Jerry D'Antonio" expect(ted.full_name).to eq "Ted Logan" expect(ted.formal_name).to eq "Ted Theodore Logan, Esq." end end end end ================================================ FILE: spec/functional/tuple_spec.rb ================================================ require 'rspec/expectations' RSpec::Matchers.define :be_a_different_tuple_than do |expected| match do |actual| actual.is_a?(Functional::Tuple) && actual.object_id != expected.object_id end end module Functional describe Tuple do context '#initialize' do it 'creates an empty tuple when given no arguments' do expect(Tuple.new).to be_empty end it 'creates an empty tuple when given an empty array' do expect(Tuple.new([])).to be_empty end it 'creates a tuple when given a single array argument' do subject = Tuple.new([:foo, :bar, :baz]) expect(subject).to_not be_empty expect(subject[0]).to eq :foo expect(subject[1]).to eq :bar expect(subject[2]).to eq :baz end it 'creates a tuple when given a single argument that responds to #to_a' do clazz = Class.new { def to_a() [:foo, :bar, :baz]; end }.new subject = Tuple.new(clazz) expect(subject).to_not be_empty expect(subject[0]).to eq :foo expect(subject[1]).to eq :bar expect(subject[2]).to eq :baz end it 'raises an exception when given a non-array argument' do expect { Tuple.new(:foo) }.to raise_error(ArgumentError) end it 'freezes the tuple' do expect(Tuple.new).to be_frozen expect(Tuple.new([])).to be_frozen expect(Tuple.new([:foo, :bar, :baz])).to be_frozen end end context '#at' do subject { Tuple.new([:foo, :bar, :baz]) } it 'returns the nth element when given a valid non-negative index' do expect(subject.at(0)).to eq :foo expect(subject.at(1)).to eq :bar expect(subject.at(2)).to eq :baz end it 'returns the nth element from the end when given a valid negative index' do expect(subject.at(-1)).to eq :baz expect(subject.at(-2)).to eq :bar expect(subject.at(-3)).to eq :foo end it 'returns nil when given a non-negative out-of-bounds index' do expect(subject.at(3)).to be_nil end it 'returns nil when given a negative out-of-bounds index' do expect(subject.at(-4)).to be_nil end it 'is aliased as #nth' do expect(subject.nth(0)).to eq :foo expect(subject.nth(1)).to eq :bar expect(subject.nth(-2)).to eq :bar expect(subject.nth(-3)).to eq :foo end it 'is aliased as #[]' do expect(subject[0]).to eq :foo expect(subject[1]).to eq :bar expect(subject[-2]).to eq :bar expect(subject[-3]).to eq :foo end end context '#fetch' do subject { Tuple.new([:foo, :bar, :baz]) } it 'returns the nth element when given a valid non-negative index' do expect(subject.fetch(0, 42)).to eq :foo expect(subject.fetch(1, 42)).to eq :bar expect(subject.fetch(2, 42)).to eq :baz end it 'returns the nth element from the end when given a valid negative index' do expect(subject.fetch(-1, 42)).to eq :baz expect(subject.fetch(-2, 42)).to eq :bar expect(subject.fetch(-3, 42)).to eq :foo end it 'returns the given default when given a non-negative out-of-bounds index' do expect(subject.fetch(3, 42)).to eq 42 end it 'returns the given default when given a negative out-of-bounds index' do expect(subject.fetch(-4, 42)).to eq 42 end end context '#length' do it 'returns 0 for an empty tuple' do expect(Tuple.new.length).to eq 0 end it 'returns the length of a non-empty tuple' do expect(Tuple.new([1, 2, 3]).length).to eq 3 end it 'is aliased a #size' do expect(Tuple.new.size).to eq 0 expect(Tuple.new([1, 2, 3]).size).to eq 3 end end context '#intersect' do it 'returns an empty tuple when self is empty' do subject = Tuple.new other = Tuple.new([1, 2, 3]) result = subject.intersect(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to be_empty end it 'returns an empty tuple when other is empty' do subject = Tuple.new([1, 2, 3]) other = Tuple.new result = subject.intersect(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to be_empty end it 'returns a tuple with all elements common to both tuples' do subject = Tuple.new([1, 2, 3]) other = Tuple.new([2, 3, 4]) result = subject.intersect(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [2, 3] end it 'removes duplicates from self' do subject = Tuple.new([1, 2, 2, 3, 3, 3]) other = Tuple.new([2, 3, 4]) result = subject.intersect(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [2, 3] end it 'removes duplicates from other' do subject = Tuple.new([1, 2, 3]) other = Tuple.new([2, 2, 3, 3, 3, 4]) result = subject.intersect(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [2, 3] end it 'operates on any other that responds to #to_a' do subject = Tuple.new([1, 2, 3]) other = Class.new { def to_a() [2, 3, 4]; end }.new result = subject.intersect(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [2, 3] end it 'is aliased as #&' do subject = Tuple.new([1, 2, 3]) other = Tuple.new([2, 3, 4]) result = subject & other expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [2, 3] end end context '#union' do it 'returns a copy of self when other is empty' do subject = Tuple.new other = Tuple.new([1, 2, 3]) result = subject.union(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2, 3] end it 'returns a copy of other when self is empty' do subject = Tuple.new([1, 2, 3]) other = Tuple.new result = subject.union(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2, 3] end it 'returns a tuple with all elements from both tuples' do subject = Tuple.new([1, 2, 3]) other = Tuple.new([1, 2, 3, 4]) result = subject.union(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2, 3, 4] end it 'removes duplicates from self' do subject = Tuple.new([1, 2, 2, 3, 3, 3]) other = Tuple.new([1, 2, 3, 4]) result = subject.union(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2, 3, 4] end it 'removes duplicates from other' do subject = Tuple.new([1, 2, 3, 4]) other = Tuple.new([1, 2, 2, 3, 3, 3]) result = subject.union(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2, 3, 4] end it 'operates on any other that responds to #to_a' do subject = Tuple.new([1, 2, 3]) other = Class.new { def to_a() [2, 3, 4]; end }.new result = subject.union(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2, 3, 4] end it 'is aliased as #|' do subject = Tuple.new([1, 2, 3]) other = Tuple.new([2, 3, 4]) result = subject | other expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2, 3, 4] end end context '#concat' do it 'returns a copy of self when other is empty' do subject = Tuple.new other = Tuple.new([1, 2, 3]) result = subject.concat(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2, 3] end it 'returns a copy of other when self is empty' do subject = Tuple.new([1, 2, 3]) other = Tuple.new result = subject.concat(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2, 3] end it 'returns a new tuple containing all of self and other in order' do subject = Tuple.new([1, 2, 3]) other = Tuple.new([4, 5, 6]) result = subject.concat(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2, 3, 4, 5, 6] end it 'does not remove duplicates from self or other' do subject = Tuple.new([1, 2, 2, 3, 3, 3]) other = Tuple.new([4, 4, 4, 5, 5, 6]) result = subject.concat(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 6] end it 'operates on any other that responds to #to_a' do subject = Tuple.new([1, 2, 3]) other = Class.new { def to_a() [4, 5, 6]; end }.new result = subject.concat(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2, 3, 4, 5, 6] end it 'is aliased as #+' do subject = Tuple.new([1, 2, 2, 3, 3, 3]) other = Tuple.new([4, 4, 4, 5, 5, 6]) result = subject + other expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 6] end end context '#diff' do it 'returns a copy of self when other is empty' do subject = Tuple.new([1, 2, 3]) other = Tuple.new result = subject.diff(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2, 3] end it 'returns an empty tuple when self is empty' do subject = Tuple.new other = Tuple.new([1, 2, 3]) result = subject.diff(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to be_empty end it 'returns an empty tuple when self and other have identical elements' do subject = Tuple.new([1, 2, 3]) other = Tuple.new([1, 2, 3]) result = subject.diff(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to be_empty end it 'returns a tuple with all elements in self not also in other' do subject = Tuple.new([1, 2, 3]) other = Tuple.new([3, 4, 5]) result = subject.diff(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2] end it 'removes duplicates from self when in other' do subject = Tuple.new([1, 2, 3, 3, 3]) other = Tuple.new([3, 4, 5]) result = subject.diff(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2] end it 'removes duplicates from other when in self' do subject = Tuple.new([1, 2, 3]) other = Tuple.new([3, 3, 3, 4, 5]) result = subject.diff(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2] end it 'operates on any other that responds to #to_a' do subject = Tuple.new([1, 2, 3]) other = Class.new { def to_a() [3, 4, 5]; end }.new result = subject.diff(other) expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2] end it 'is aliased as #-' do subject = Tuple.new([1, 2, 3]) other = Tuple.new([3, 4, 5]) result = subject - other expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2] end end context '#repeat' do it 'returns an empty tuple when multipled by zero' do subject = Tuple.new([1, 2, 3]) result = subject.repeat(0) expect(result).to be_a_different_tuple_than(subject) expect(result).to be_empty end it 'returns a copy of self when multipled by one' do subject = Tuple.new([1, 2, 3]) result = subject.repeat(1) expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2, 3] end it 'returns a tuple containing elements from self repeated n times' do subject = Tuple.new([1, 2, 3]) result = subject.repeat(3) expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2, 3, 1, 2, 3, 1, 2, 3] end it 'raises an exception when given a negative argument' do subject = Tuple.new([1, 2, 3]) expect { subject.repeat(-2) }.to raise_error(ArgumentError) end it 'is aliased as #*' do subject = Tuple.new([1, 2, 3]) result = subject * 3 expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2, 3, 1, 2, 3, 1, 2, 3] end end context '#uniq' do it 'returns a empty tuple when empty' do subject = Tuple.new result = subject.uniq expect(result).to be_a_different_tuple_than(subject) expect(result).to be_empty end it 'returns a copy of self when there are no duplicate elements' do subject = Tuple.new([1, 2, 3]) result = subject.uniq expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2, 3] end it 'returns a new tuple with duplicates removed' do subject = Tuple.new([1, 2, 2, 3, 3, 3]) result = subject.uniq expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [1, 2, 3] end end context '#each' do it 'returns an Enumerable when no block given' do subject = Tuple.new([1, 2, 3]) expect(subject.each).to be_a Enumerable end it 'enumerates over each element' do result = [] subject = Tuple.new([1, 2, 2, 3, 3, 3]) subject.each{|item| result << item } expect(result).to eq [1, 2, 2, 3, 3, 3] end it 'does not call the block when empty' do result = false Tuple.new.each{|item| expected = true} expect(result).to be false end end context '#each_with_index' do it 'returns an Enumerable when no block given' do subject = Tuple.new([1, 2, 3]) expect(subject.each_with_index).to be_a Enumerable end it 'enumerates over each element and index pair' do result = {} subject = Tuple.new([1, 2, 2, 3, 3, 3]) subject.each_with_index{|item, index| result[index] = item } expected = { 0 => 1, 1 => 2, 2 => 2, 3 => 3, 4 => 3, 5 => 3, } expect(result).to eq expected end it 'does not call the block when empty' do result = false Tuple.new.each_with_index{|item, index| expected = true} expect(result).to be false end end context '#sequence' do it 'returns an Enumerable when no block given' do subject = Tuple.new([1, 2, 3]) expect(subject.sequence).to be_a Enumerable end it 'enumerates over each element' do result = [] subject = Tuple.new([1, 2, 2, 3, 3, 3]) subject.sequence{|item, rest| result << item } expect(result).to eq [1, 2, 2, 3, 3, 3] end it 'yields rest of tuple for each element' do result = [] subject = Tuple.new([1, 2, 3, 4]) subject.sequence{|item, rest| result = rest; break } expect(result).to eq [2, 3, 4] end it 'yields rest of tuple as a tuple' do result = [] subject = Tuple.new([1, 2, 3, 4]) subject.sequence{|item, rest| result = rest; break } expect(result).to be_a_different_tuple_than(subject) end it 'yields an empty tuple for rest when on last element' do result = nil subject = Tuple.new([1]) subject.sequence{|item, rest| result = rest } expect(result).to be_a_different_tuple_than(subject) expect(result).to eq [] end it 'does not call the block when empty' do result = false Tuple.new.sequence{|item, rest| expected = true} expect(result).to be false end end context '#eql?' do it 'returns true when compared to a tuple with identical elements' do subject = Tuple.new([1, 2, 3]) other = Tuple.new([1, 2, 3]) expect(subject.eql?(other)).to be true end it 'returns false when given a tuple with different elements' do subject = Tuple.new([1, 2, 3]) other = Tuple.new([2, 3, 4]) expect(subject.eql?(other)).to be false end it 'operates on any other that responds to #to_a' do subject = Tuple.new([1, 2, 3]) other = Class.new { def to_a() [1, 2, 3]; end }.new expect(subject.eql?(other)).to be true end it 'is aliased as #==' do subject = Tuple.new([1, 2, 3]) identical = Tuple.new([1, 2, 3]) different = Tuple.new([2, 3, 4]) expect(subject == identical).to be true expect(subject == different).to be false end end context '#empty?' do it 'returns true when there are no elements' do subject = Tuple.new expect(subject.empty?).to be true end it 'returns false when there are one or more elements' do subject = Tuple.new([1, 2, 3]) expect(subject.empty?).to be false end end context '#first' do it 'returns nil when empty' do subject = Tuple.new expect(subject.first).to be nil end it 'returns the first element when not empty' do subject = Tuple.new([1, 2, 3]) expect(subject.first).to eq 1 end it 'is aliased as #head' do expect(Tuple.new.head).to be nil expect(Tuple.new([1, 2, 3]).head).to eq 1 end end context '#rest' do it 'returns an empty tuple when empty' do subject = Tuple.new expect(subject.rest).to be_a_different_tuple_than(subject) expect(subject.rest).to be_empty end it 'returns an empty tuple when there is only one item' do subject = Tuple.new([1]) expect(subject.rest).to be_a_different_tuple_than(subject) expect(subject.rest).to be_empty end it 'returns a tuple with all but the first element when not empty' do subject = Tuple.new([1, 2, 3]) expect(subject.rest).to be_a_different_tuple_than(subject) expect(subject.rest).to eq [2, 3] end it 'is aliased as #tail' do expect(Tuple.new.rest).to be_a_different_tuple_than(subject) expect(Tuple.new.rest).to be_empty expect(Tuple.new([1, 2, 3]).rest).to be_a_different_tuple_than(subject) expect(Tuple.new([1, 2, 3]).rest).to eq [2, 3] end end context '#to_a' do it 'returns an empty array when empty' do subject = Tuple.new.to_a expect(subject).to be_a Array expect(subject).to be_empty end it 'returns an array with the same elements as self' do subject = Tuple.new([1, 2, 3]).to_a expect(subject).to be_a Array expect(subject).to eq [1, 2, 3] end it 'returns a non-frozen array' do expect(Tuple.new.to_a).to_not be_frozen expect(Tuple.new([1, 2, 3]).to_a).to_not be_frozen end it 'is aliased as #to_ary' do subject = Tuple.new([1, 2, 3]).to_ary expect(subject).to be_a Array expect(subject).to eq [1, 2, 3] end end context 'reflection' do specify '#inspect begins with the class name' do subject = Tuple.new([1, 2, 3]) expect(subject.inspect).to match(/^#<#{described_class}:\s+/) end specify '#inspect includes a list of all elements' do subject = Tuple.new([1, 2, 3]) expect(subject.inspect).to match(/\s+\[1, 2, 3\]>$/) expect(Tuple.new.inspect).to match(/\s+\[\]>$/) end specify '#to_s returns the same string an an array with the same elements' do expect(Tuple.new.to_s).to eq [].to_s expect(Tuple.new([1, 2, 3]).to_s).to eq [1, 2, 3].to_s end end end end ================================================ FILE: spec/functional/type_check_spec.rb ================================================ module Functional describe TypeCheck do context 'Type?' do it 'returns true when value is of any of the types' do target = 'foo' expect(TypeCheck.Type?(target, String, Array, Hash)).to be true end it 'returns false when value is not of any of the types' do target = 'foo' expect(TypeCheck.Type?(target, Fixnum, Array, Hash)).to be false end end context 'Type!' do it 'returns the value when value is of any of the types' do target = 'foo' expect(TypeCheck.Type!(target, String, Array, Hash)).to be target end it 'raises an exception when value is not of any of the types' do target = 'foo' expect { TypeCheck.Type!(target, Fixnum, Array, Hash) }.to raise_error(TypeError) end end context 'Match?' do it 'returns true when value is an exact match for at least one of the types' do target = 'foo' expect(TypeCheck.Match?(target, String, Array, Hash)).to be true end it 'returns false when value is not an exact match for at least one of the types' do target = 'foo' expect(TypeCheck.Match?(target, Fixnum, Array, Hash)).to be false end end context 'Match!' do it 'returns the value when value is an exact match for at least one of the types' do target = 'foo' expect(TypeCheck.Match!(target, String, Array, Hash)).to eq target end it 'raises an exception when value is not an exact match for at least one of the types' do target = 'foo' expect { expect(TypeCheck.Match!(target, Fixnum, Array, Hash)).to eq target }.to raise_error(TypeError) end end context 'Child?' do it 'returns true if value is a class and is also a match or subclass of one of types' do target = String expect(TypeCheck.Child?(target, Comparable, Array, Hash)).to be true end it 'returns false if value is not a class' do target = 'foo' expect(TypeCheck.Child?(target, Comparable, Array, Hash)).to be false end it 'returns false if value is not a subclass/match for any of the types' do target = Fixnum expect(TypeCheck.Child?(target, Symbol, Array, Hash)).to be false end end context 'Child!' do it 'returns the value if value is a class and is also a match or subclass of one of types' do target = String expect(TypeCheck.Child!(target, Comparable, Array, Hash)).to eq target end it 'raises an exception if value is not a class' do target = 'foo' expect { TypeCheck.Child!(target, Comparable, Array, Hash) }.to raise_error(TypeError) end it 'raises an exception if value is not a subclass/match for any of the types' do target = Fixnum expect { TypeCheck.Child!(target, Symbol, Array, Hash) }.to raise_error(TypeError) end end end end ================================================ FILE: spec/functional/union_spec.rb ================================================ require_relative 'abstract_struct_shared' module Functional describe Union do let!(:expected_fields){ [:a, :b, :c] } let!(:expected_values){ [42, nil, nil] } let(:struct_class) { Union.new(*expected_fields) } let(:struct_object) { struct_class.send(struct_class.fields.first, 42) } let(:other_object) { struct_class.send(struct_class.fields.first, Object.new) } it_should_behave_like :abstract_struct context 'definition' do it 'registers the new class with Record when given a string name' do Union.new('Foo', :foo, :bar, :baz) expect(defined?(Union::Foo)).to eq 'constant' end end context 'factories' do specify 'exist for each field' do expected_fields.each do |field| expect(struct_class).to respond_to(field) end end specify 'require a value' do expected_fields.each do |field| expect(struct_class.method(field).arity).to eq 1 end end specify 'set the field appropriately' do clazz = Union.new(:foo, :bar) obj = clazz.foo(10) expect(obj.field).to eq :foo end specify 'set the value appropriately' do clazz = Union.new(:foo, :bar) obj = clazz.foo(10) expect(obj.value).to eq 10 end specify 'return a frozen union' do clazz = Union.new(:foo, :bar) expect(clazz.foo(10)).to be_frozen end specify 'force #new to be private' do clazz = Union.new(:foo, :bar) expect { clazz.new }.to raise_error(NoMethodError) end end context 'readers' do specify '#field returns the appropriate field' do clazz = Union.new(:foo, :bar) expect(clazz.foo(10).field).to eq :foo end specify '#value returns the appropriate field' do clazz = Union.new(:foo, :bar) expect(clazz.foo(10).value).to eq 10 end specify 'return the appropriate value for the set field' do clazz = Union.new(:foo, :bar) expect(clazz.foo(10).foo).to eq 10 end specify 'return nil for the unset field' do clazz = Union.new(:foo, :bar, :baz) expect(clazz.foo(10).bar).to be_nil expect(clazz.foo(10).baz).to be_nil end end context 'predicates' do specify 'exist for each field' do expected_fields.each do |field| predicate = "#{field}?".to_sym expect(struct_object).to respond_to(predicate) expect(struct_object.method(predicate).arity).to eq 0 end end specify 'return true for the set field' do clazz = Union.new(:foo, :bar) expect(clazz.foo(10).foo?).to be true end specify 'return false for the unset fields' do clazz = Union.new(:foo, :bar, :baz) expect(clazz.foo(10).bar?).to be false expect(clazz.foo(10).baz?).to be false end end end end ================================================ FILE: spec/functional/value_struct_spec.rb ================================================ require 'ostruct' module Functional describe ValueStruct do context 'instanciation' do specify 'raises an exception when no arguments given' do expect { ValueStruct.new }.to raise_error(ArgumentError) end specify 'with a hash sets fields using has values' do subject = ValueStruct.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 = ValueStruct.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 #each_pair' do clazz = Class.new do def each_pair(&block) {answer: 42, harmless: 'mostly'}.each_pair(&block) end end struct = clazz.new subject = ValueStruct.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 { ValueStruct.new(:bogus) }.to raise_error(ArgumentError) end end context 'set fields' do subject { ValueStruct.new(foo: 42, bar: "Don't Panic") } 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 end context 'unset fields' do subject { ValueStruct.new(foo: 42, bar: "Don't Panic") } specify 'have a magic predicate that always returns false' do expect(subject.baz?).to be false end end context 'accessors' do let!(:field_value_pairs) { {foo: 1, bar: :two, baz: 'three'} } subject { ValueStruct.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? 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 = ValueStruct.new(harmless: 'mostly') expect(subject.set?(:harmless)).to be true end specify '#fetch gets the value of a set field' do subject = ValueStruct.new(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 raise_error(NoMethodError) end specify '#to_h returns the key/value pairs for all set values' do subject = ValueStruct.new(field_value_pairs) expect(subject.to_h).to eq field_value_pairs expect(subject.to_h).to_not be_frozen end specify '#each_pair returns an Enumerable when no block given' do subject = ValueStruct.new(field_value_pairs) expect(subject.each_pair).to be_a Enumerable end specify '#each_pair enumerates over each field/value pair' do subject = ValueStruct.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 = ValueStruct.new(foo: 1, 'bar' => :two, baz: 'three') second = ValueStruct.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 = ValueStruct.new(foo: 1, 'bar' => :two, baz: 'three') second = ValueStruct.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 = ValueStruct.new(foo: 1, 'bar' => :two, baz: 'three') second = ValueStruct.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 ValueStruct' do attributes = {answer: 42, harmless: 'mostly'} clazz = Class.new do def to_h; {answer: 42, harmless: 'mostly'}; end end other = clazz.new subject = ValueStruct.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 = ValueStruct.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 = ValueStruct.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 = ValueStruct.new(foo: 1, 'bar' => :two, baz: 'three') expect(subject.to_s).to eq subject.inspect end end end end ================================================ FILE: spec/spec_helper.rb ================================================ require 'simplecov' require 'coveralls' SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter ] SimpleCov.start do project_name 'Functional Ruby' add_filter '/spec/' end #require 'coveralls' #Coveralls.wear! #require 'codeclimate-test-reporter' #CodeClimate::TestReporter.start require 'functional' # import all the support files Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each { |f| require File.expand_path(f) } RSpec.configure do |config| config.order = 'random' config.before(:suite) do end config.before(:each) do end config.after(:each) do end end ================================================ FILE: spec/support/.gitignore ================================================ ================================================ FILE: tasks/.gitignore ================================================ ================================================ FILE: tasks/metrics.rake ================================================ desc 'Display LOC (lines of code) report' task :loc do sh 'countloc -r lib' end desc 'Display code quality analysis report' task :critic do sh 'rubycritic lib --path critic' end ================================================ FILE: tasks/update_doc.rake ================================================ require 'yard' YARD::Rake::YardocTask.new root = File.expand_path File.join(File.dirname(__FILE__), '..') namespace :yard do cmd = lambda do |command| puts ">> executing: #{command}" system command or raise "#{command} failed" end desc 'Pushes generated documentation to github pages: http://jdantonio.github.io/functional-ruby/' task :push => [:setup, :yard] do message = Dir.chdir(root) do `git log -n 1 --oneline`.strip end puts "Generating commit: #{message}" Dir.chdir "#{root}/yardoc" do cmd.call "git add -A" cmd.call "git commit -m '#{message}'" cmd.call 'git push origin gh-pages' end end desc 'Setups second clone in ./yardoc dir for pushing doc to github' task :setup do unless File.exist? "#{root}/yardoc/.git" cmd.call "rm -rf #{root}/yardoc" if File.exist?("#{root}/yardoc") Dir.chdir "#{root}" do cmd.call 'git clone --single-branch --branch gh-pages git@github.com:jdantonio/functional-ruby.git ./yardoc' end end Dir.chdir "#{root}/yardoc" do cmd.call 'git fetch origin' cmd.call 'git reset --hard origin/gh-pages' end end end