├── spec ├── .gitignore ├── support │ └── .gitignore ├── spec_helper.rb └── functional │ ├── union_spec.rb │ ├── type_check_spec.rb │ ├── delay_spec.rb │ ├── abstract_struct_shared.rb │ ├── final_var_spec.rb │ ├── memo_spec.rb │ ├── complex_pattern_matching_spec.rb │ ├── value_struct_spec.rb │ ├── either_spec.rb │ ├── protocol_spec.rb │ ├── option_spec.rb │ ├── final_struct_spec.rb │ └── record_spec.rb ├── tasks ├── .gitignore ├── metrics.rake └── update_doc.rake ├── .rspec ├── .coveralls.yml ├── lib ├── functional │ ├── version.rb │ ├── synchronization.rb │ ├── method_signature.rb │ ├── memo.rb │ ├── type_check.rb │ ├── delay.rb │ ├── abstract_struct.rb │ ├── value_struct.rb │ ├── final_var.rb │ ├── union.rb │ ├── protocol.rb │ ├── pattern_matching.rb │ ├── protocol_info.rb │ ├── option.rb │ ├── record.rb │ ├── final_struct.rb │ └── either.rb └── functional.rb ├── .yardopts ├── .gitignore ├── Gemfile ├── appveyor.yml ├── .travis.yml ├── Rakefile ├── functional_ruby.gemspec ├── LICENSE ├── doc ├── memoize.rb ├── memo.md └── protocol.md ├── CODE_OF_CONDUCT.md ├── CHANGELOG.md └── README.md /spec/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tasks/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/support/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --format progress 3 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: M3JnILwxCIYb4OjWvyxBJkib9xsAGdnek 2 | -------------------------------------------------------------------------------- /lib/functional/version.rb: -------------------------------------------------------------------------------- 1 | module Functional 2 | 3 | # The current gem version. 4 | VERSION = '1.3.0' 5 | end 6 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --protected 2 | --no-private 3 | --embed-mixins 4 | --output-dir ./yardoc 5 | --markup markdown 6 | --title=Functional Ruby 7 | --template default 8 | 9 | ./lib/**/*.rb 10 | - 11 | README.md 12 | CHANGELOG.md 13 | LICENSE 14 | -------------------------------------------------------------------------------- /tasks/metrics.rake: -------------------------------------------------------------------------------- 1 | desc 'Display LOC (lines of code) report' 2 | task :loc do 3 | sh 'countloc -r lib' 4 | end 5 | 6 | desc 'Display code quality analysis report' 7 | task :critic do 8 | sh 'rubycritic lib --path critic' 9 | end 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | .rspec-local 3 | *.gem 4 | lib/1.8 5 | lib/1.9 6 | lib/2.0 7 | .rvmrc 8 | .ruby-version 9 | .ruby-gemset 10 | .bundle/* 11 | .yardoc/* 12 | yardoc/* 13 | tmp/* 14 | man/* 15 | *.tmproj 16 | rdoc/* 17 | *.orig 18 | *.BACKUP.* 19 | *.BASE.* 20 | *.LOCAL.* 21 | *.REMOTE.* 22 | git_pull.txt 23 | coverage 24 | critic 25 | .DS_Store 26 | TAGS 27 | tmtags 28 | *.sw? 29 | .idea 30 | .rbx/* 31 | lib/*.bundle 32 | lib/*.so 33 | lib/*.jar 34 | ext/*.bundle 35 | ext/*.so 36 | ext/*.jar 37 | pkg 38 | *.gem 39 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem 'rake', '~> 12.3.0' 7 | end 8 | 9 | group :testing do 10 | gem 'rspec', '~> 3.7.0' 11 | gem 'simplecov', '~> 0.14.1', platforms: :mri, require: false 12 | gem 'coveralls', '~> 0.8.21', require: false 13 | end 14 | 15 | group :documentation do 16 | gem 'countloc', '~> 0.4.0', platforms: :mri, require: false 17 | gem 'yard', '~> 0.9.12', require: false 18 | gem 'redcarpet', '~> 3.4.0', platforms: :mri # understands github markdown 19 | end 20 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | install: 2 | - SET PATH=C:\Ruby%ruby_version%\bin;%PATH% 3 | - SET PATH=C:\MinGW\bin;%PATH% 4 | - SET RAKEOPT=-rdevkit 5 | - ruby --version 6 | - gem --version 7 | - bundle install 8 | 9 | build: off 10 | 11 | test_script: 12 | - bundle exec rake 13 | 14 | environment: 15 | matrix: 16 | - ruby_version: "200" 17 | - ruby_version: "200-x64" 18 | - ruby_version: "21" 19 | - ruby_version: "21-x64" 20 | - ruby_version: "22" 21 | - ruby_version: "22-x64" 22 | 23 | #matrix: 24 | #allow_failures: 25 | #- ruby_version: "193" 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | - 2.2.3 5 | - 2.2.2 6 | - 2.2.1 7 | - 2.1.5 8 | - 2.1.4 9 | - 2.0.0 10 | - ruby-head 11 | - jruby-1.7.19 12 | - jruby-9.0.1.0 13 | - jruby-9.0.3.0 14 | - jruby-9.0.4.0 15 | - jruby-head 16 | - rbx-2 17 | 18 | jdk: 19 | - oraclejdk8 20 | 21 | sudo: false 22 | 23 | branches: 24 | only: 25 | - master 26 | 27 | matrix: 28 | allow_failures: 29 | - rvm: ruby-head 30 | - rvm: jruby-head 31 | - rvm: jruby-9.0.1.0 32 | - rvm: rbx-2 33 | 34 | script: "CODECLIMATE_REPO_TOKEN=65d4787423f734f5cf6d2b3f9be88e481802e50af0879e8ed66971f972d70894 bundle exec rake" 35 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | $:.push File.join(File.dirname(__FILE__), 'lib') 2 | 3 | GEMSPEC = Gem::Specification.load('functional-ruby.gemspec') 4 | 5 | require 'bundler/gem_tasks' 6 | require 'rspec' 7 | require 'rspec/core/rake_task' 8 | 9 | require 'functional' 10 | 11 | Bundler::GemHelper.install_tasks 12 | 13 | Dir.glob('tasks/**/*.rake').each do|rakefile| 14 | load rakefile 15 | end 16 | 17 | RSpec::Core::RakeTask.new(:spec) do |t| 18 | t.rspec_opts = '--color --backtrace --format documentation' 19 | end 20 | 21 | RSpec::Core::RakeTask.new(:travis_spec) do |t| 22 | t.rspec_opts = '--tag ~@not_on_travis' 23 | end 24 | 25 | task :default => [:travis_spec] 26 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | require 'coveralls' 3 | 4 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ 5 | SimpleCov::Formatter::HTMLFormatter, 6 | Coveralls::SimpleCov::Formatter 7 | ] 8 | 9 | SimpleCov.start do 10 | project_name 'Functional Ruby' 11 | add_filter '/spec/' 12 | end 13 | 14 | #require 'coveralls' 15 | #Coveralls.wear! 16 | #require 'codeclimate-test-reporter' 17 | #CodeClimate::TestReporter.start 18 | 19 | require 'functional' 20 | 21 | # import all the support files 22 | Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each { |f| require File.expand_path(f) } 23 | 24 | RSpec.configure do |config| 25 | config.order = 'random' 26 | 27 | config.before(:suite) do 28 | end 29 | 30 | config.before(:each) do 31 | end 32 | 33 | config.after(:each) do 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/functional.rb: -------------------------------------------------------------------------------- 1 | require 'functional/delay' 2 | require 'functional/either' 3 | require 'functional/final_struct' 4 | require 'functional/final_var' 5 | require 'functional/memo' 6 | require 'functional/option' 7 | require 'functional/pattern_matching' 8 | require 'functional/protocol' 9 | require 'functional/protocol_info' 10 | require 'functional/record' 11 | require 'functional/tuple' 12 | require 'functional/type_check' 13 | require 'functional/union' 14 | require 'functional/value_struct' 15 | require 'functional/version' 16 | 17 | Functional::SpecifyProtocol(:Disposition) do 18 | instance_method :value, 0 19 | instance_method :value?, 0 20 | instance_method :reason, 0 21 | instance_method :reason?, 0 22 | instance_method :fulfilled?, 0 23 | instance_method :rejected?, 0 24 | end 25 | 26 | # Erlang, Clojure, and Go inspired functional programming tools to Ruby. 27 | module Functional 28 | 29 | # Infinity 30 | Infinity = 1/0.0 31 | 32 | # Not a number 33 | NaN = 0/0.0 34 | end 35 | -------------------------------------------------------------------------------- /functional_ruby.gemspec: -------------------------------------------------------------------------------- 1 | $LOAD_PATH << File.expand_path('../lib', __FILE__) 2 | 3 | require 'functional/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'functional-ruby' 7 | s.version = Functional::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.author = "Jerry D'Antonio" 10 | s.email = 'jerry.dantonio@gmail.com' 11 | s.homepage = 'https://github.com/jdantonio/functional-ruby/' 12 | s.summary = 'Erlang, Clojure, Haskell, and Functional Java inspired functional programming tools for Ruby.' 13 | s.license = 'MIT' 14 | s.date = Time.now.strftime('%Y-%m-%d') 15 | 16 | s.description = <<-EOF 17 | A gem for adding functional programming tools to Ruby. Inspired by Erlang, Clojure, Haskell, and Functional Java. 18 | EOF 19 | 20 | s.files = Dir['README*', 'LICENSE*', 'CHANGELOG*'] 21 | s.files += Dir['{lib}/**/*'] 22 | s.test_files = Dir['{spec}/**/*'] 23 | s.extra_rdoc_files = Dir['README*', 'LICENSE*', 'CHANGELOG*'] 24 | s.extra_rdoc_files += Dir['{doc}/**/*.{txt,md}'] 25 | s.require_paths = ['lib'] 26 | 27 | s.required_ruby_version = '>= 2.0.0' 28 | end 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Jerry D'Antonio -- released under the MIT license. 2 | 3 | http://www.opensource.org/licenses/mit-license.php 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /tasks/update_doc.rake: -------------------------------------------------------------------------------- 1 | require 'yard' 2 | YARD::Rake::YardocTask.new 3 | 4 | root = File.expand_path File.join(File.dirname(__FILE__), '..') 5 | 6 | namespace :yard do 7 | 8 | cmd = lambda do |command| 9 | puts ">> executing: #{command}" 10 | system command or raise "#{command} failed" 11 | end 12 | 13 | desc 'Pushes generated documentation to github pages: http://jdantonio.github.io/functional-ruby/' 14 | task :push => [:setup, :yard] do 15 | 16 | message = Dir.chdir(root) do 17 | `git log -n 1 --oneline`.strip 18 | end 19 | puts "Generating commit: #{message}" 20 | 21 | Dir.chdir "#{root}/yardoc" do 22 | cmd.call "git add -A" 23 | cmd.call "git commit -m '#{message}'" 24 | cmd.call 'git push origin gh-pages' 25 | end 26 | 27 | end 28 | 29 | desc 'Setups second clone in ./yardoc dir for pushing doc to github' 30 | task :setup do 31 | 32 | unless File.exist? "#{root}/yardoc/.git" 33 | cmd.call "rm -rf #{root}/yardoc" if File.exist?("#{root}/yardoc") 34 | Dir.chdir "#{root}" do 35 | cmd.call 'git clone --single-branch --branch gh-pages git@github.com:jdantonio/functional-ruby.git ./yardoc' 36 | end 37 | end 38 | Dir.chdir "#{root}/yardoc" do 39 | cmd.call 'git fetch origin' 40 | cmd.call 'git reset --hard origin/gh-pages' 41 | end 42 | 43 | end 44 | 45 | end 46 | -------------------------------------------------------------------------------- /doc/memoize.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $LOAD_PATH << File.expand_path('../../lib', __FILE__) 3 | 4 | require 'functional' 5 | 6 | class Factors 7 | include Functional::Memo 8 | 9 | def self.sum_of(number) 10 | of(number).reduce(:+) 11 | end 12 | 13 | def self.of(number) 14 | (1..number).select {|i| factor?(number, i)} 15 | end 16 | 17 | def self.factor?(number, potential) 18 | number % potential == 0 19 | end 20 | 21 | def self.perfect?(number) 22 | sum_of(number) == 2 * number 23 | end 24 | 25 | def self.abundant?(number) 26 | sum_of(number) > 2 * number 27 | end 28 | 29 | def self.deficient?(number) 30 | sum_of(number) < 2 * number 31 | end 32 | 33 | memoize(:sum_of) 34 | memoize(:of) 35 | end 36 | 37 | require 'benchmark' 38 | require 'pp' 39 | 40 | def memory_usage 41 | `ps ax -o pid,rss | grep -E "^[[:space:]]*#{$$}"`.strip.split.map(&:to_i) 42 | end 43 | 44 | def print_memory_usage 45 | pid, size = memory_usage 46 | puts "Memory used by process #{pid} at #{Time.now} is #{size}" 47 | end 48 | 49 | def run_benchmark(n = 10000) 50 | 51 | puts "Benchmarks for #{n} numbers..." 52 | puts 53 | 54 | puts 'With no memoization...' 55 | stats = Benchmark.measure do 56 | Factors.sum_of(n) 57 | end 58 | puts stats 59 | 60 | 2.times do 61 | puts 62 | puts 'With memoization...' 63 | stats = Benchmark.measure do 64 | Factors.sum_of(n) 65 | end 66 | puts stats 67 | end 68 | end 69 | 70 | if $0 == __FILE__ 71 | run_benchmark(10_000_000) 72 | end 73 | 74 | __END__ 75 | 76 | $ ./doc/memoize.rb 77 | Benchmarks for 10000000 numbers... 78 | 79 | With no memoization... 80 | 1.660000 0.000000 1.660000 ( 1.657253) 81 | 82 | With memoization... 83 | 0.000000 0.000000 0.000000 ( 0.000019) 84 | 85 | With memoization... 86 | 0.000000 0.000000 0.000000 ( 0.000008) 87 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | 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. 4 | 5 | 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. 6 | 7 | Examples of unacceptable behavior by participants include: 8 | 9 | * The use of sexualized language or imagery 10 | * Personal attacks 11 | * Trolling or insulting/derogatory comments 12 | * Public or private harassment 13 | * Publishing other's private information, such as physical or electronic addresses, without explicit permission 14 | * Other unethical or unprofessional conduct. 15 | 16 | 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. 17 | 18 | This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. 19 | 20 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 21 | 22 | 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/) 23 | -------------------------------------------------------------------------------- /lib/functional/synchronization.rb: -------------------------------------------------------------------------------- 1 | module Functional 2 | 3 | # @!visibility private 4 | # 5 | # Based on work originally done by Petr Chalupa (@pitr-ch) in Concurrent Ruby. 6 | # https://github.com/ruby-concurrency/concurrent-ruby/blob/master/lib/concurrent/synchronization/object.rb 7 | module Synchronization 8 | 9 | if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby' 10 | 11 | require 'jruby' 12 | 13 | # @!visibility private 14 | class Object 15 | 16 | # @!visibility private 17 | def initialize(*args) 18 | end 19 | 20 | protected 21 | 22 | # @!visibility private 23 | def synchronize 24 | JRuby.reference0(self).synchronized { yield } 25 | end 26 | 27 | # @!visibility private 28 | def ensure_ivar_visibility! 29 | # relying on undocumented behavior of JRuby, ivar access is volatile 30 | end 31 | end 32 | 33 | elsif defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx' 34 | 35 | # @!visibility private 36 | class Object 37 | 38 | # @!visibility private 39 | def initialize(*args) 40 | end 41 | 42 | protected 43 | 44 | # @!visibility private 45 | def synchronize(&block) 46 | Rubinius.synchronize(self, &block) 47 | end 48 | 49 | # @!visibility private 50 | def ensure_ivar_visibility! 51 | # Rubinius instance variables are not volatile so we need to insert barrier 52 | Rubinius.memory_barrier 53 | end 54 | end 55 | 56 | else 57 | 58 | require 'thread' 59 | 60 | # @!visibility private 61 | class Object 62 | 63 | # @!visibility private 64 | def initialize(*args) 65 | @__lock__ = ::Mutex.new 66 | @__condition__ = ::ConditionVariable.new 67 | end 68 | 69 | protected 70 | 71 | # @!visibility private 72 | def synchronize 73 | if @__lock__.owned? 74 | yield 75 | else 76 | @__lock__.synchronize { yield } 77 | end 78 | end 79 | 80 | # @!visibility private 81 | def ensure_ivar_visibility! 82 | # relying on undocumented behavior of CRuby, GVL acquire has lock which ensures visibility of ivars 83 | # https://github.com/ruby/ruby/blob/ruby_2_2/thread_pthread.c#L204-L211 84 | end 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Current Release v1.3.0 (October 4, 2015) 2 | 3 | * Pattern match now check arity of pattern and block 4 | * `PatternMatching::ALL` pattern now should be presented as variable length args (*args) 5 | * `NoMethodError` and `ArgumentError` raised from method block won't be catched anymore by lib 6 | 7 | ### Release v1.2.0 (July 10, 2015) 8 | 9 | * `Record` classes can be declared with a type/protocol specification for type safety. 10 | * Improved documentation 11 | * Improved tests 12 | * Better synchronization (thread safety) on all platforms 13 | * Continuous integration run on both Linux (Travis CI) and Windows (AppVeyor) 14 | 15 | ### Release v1.1.0 (August 12, 2014) 16 | 17 | * A simple implementation of [tuple](http://en.wikipedia.org/wiki/Tuple), an 18 | immutable, fixed-length list/array/vector-like data structure. 19 | * `FinalStruct`, a variation on Ruby's `OpenStruct` in which all fields are "final" (meaning 20 | that new fields can be arbitrarily added but once set each field becomes immutable). 21 | * `FinalVar`, a thread safe object that holds a single value and is "final" (meaning 22 | that the value can be set at most once after which it becomes immutable). 23 | 24 | ### Release v1.0.0 (July 30, 2014) 25 | 26 | * Protocol specifications inspired by Clojure [protocol](http://clojure.org/protocols), 27 | Erlang [behavior](http://www.erlang.org/doc/design_principles/des_princ.html#id60128), 28 | and Objective-C [protocol](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithProtocols/WorkingwithProtocols.html) 29 | * Function overloading with Erlang-style [function](http://erlang.org/doc/reference_manual/functions.html) 30 | [pattern matching](http://erlang.org/doc/reference_manual/patterns.html) 31 | * Simple, immutable data structures, such as *record* and *union*, inspired by 32 | [Clojure](http://clojure.org/datatypes), [Erlang](http://www.erlang.org/doc/reference_manual/records.html), 33 | and [others](http://en.wikipedia.org/wiki/Union_type) 34 | * `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) 35 | * [Memoization](http://en.wikipedia.org/wiki/Memoization) of class methods based on Clojure [memoize](http://clojuredocs.org/clojure_core/clojure.core/memoize) 36 | * Lazy execution with a `Delay` class based on Clojure [delay](http://clojuredocs.org/clojure_core/clojure.core/delay) 37 | -------------------------------------------------------------------------------- /lib/functional/method_signature.rb: -------------------------------------------------------------------------------- 1 | module Functional 2 | 3 | module PatternMatching 4 | 5 | # @!visibility private 6 | # 7 | # Helper functions used when pattern matching runtime arguments against 8 | # a method defined with the `defn` function of Functional::PatternMatching. 9 | module MethodSignature 10 | extend self 11 | 12 | # Do the given arguments match the given function pattern? 13 | # 14 | # @return [Boolean] true when there is a match else false 15 | def match?(pattern, args) 16 | return false unless valid_pattern?(args, pattern) 17 | 18 | pattern.length.times.all? do |index| 19 | param = pattern[index] 20 | arg = args[index] 21 | 22 | all_param_and_last_arg?(pattern, param, index) || 23 | arg_is_type_of_param?(param, arg) || 24 | hash_param_with_matching_arg?(param, arg) || 25 | param_matches_arg?(param, arg) 26 | end 27 | end 28 | 29 | # Is the given pattern a valid pattern with respect to the given 30 | # runtime arguments? 31 | # 32 | # @return [Boolean] true when the pattern is valid else false 33 | def valid_pattern?(args, pattern) 34 | (pattern.last == PatternMatching::ALL && args.length >= pattern.length) \ 35 | || (args.length == pattern.length) 36 | end 37 | 38 | # Is this the last parameter and is it `ALL`? 39 | # 40 | # @return [Boolean] true when matching else false 41 | def all_param_and_last_arg?(pattern, param, index) 42 | param == PatternMatching::ALL && index+1 == pattern.length 43 | end 44 | 45 | # Is the parameter a class and is the provided argument an instance 46 | # of that class? 47 | # 48 | # @return [Boolean] true when matching else false 49 | def arg_is_type_of_param?(param, arg) 50 | param.is_a?(Class) && arg.is_a?(param) 51 | end 52 | 53 | # Is the given parameter a Hash and does it match the given 54 | # runtime argument? 55 | # 56 | # @return [Boolean] true when matching else false 57 | def hash_param_with_matching_arg?(param, arg) 58 | param.is_a?(Hash) && arg.is_a?(Hash) && ! param.empty? && param.all? do |key, value| 59 | arg.has_key?(key) && (value == PatternMatching::UNBOUND || arg[key] == value) 60 | end 61 | end 62 | 63 | # Does the given parameter exactly match the given runtime 64 | # argument or is the parameter `UNBOUND`? 65 | # 66 | # @return [Boolean] true when matching else false 67 | def param_matches_arg?(param, arg) 68 | param == PatternMatching::UNBOUND || param == arg 69 | end 70 | end 71 | private_constant :MethodSignature 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/functional/union_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'abstract_struct_shared' 2 | 3 | module Functional 4 | 5 | describe Union do 6 | 7 | let!(:expected_fields){ [:a, :b, :c] } 8 | let!(:expected_values){ [42, nil, nil] } 9 | 10 | let(:struct_class) { Union.new(*expected_fields) } 11 | let(:struct_object) { struct_class.send(struct_class.fields.first, 42) } 12 | let(:other_object) { struct_class.send(struct_class.fields.first, Object.new) } 13 | 14 | it_should_behave_like :abstract_struct 15 | 16 | context 'definition' do 17 | 18 | it 'registers the new class with Record when given a string name' do 19 | Union.new('Foo', :foo, :bar, :baz) 20 | expect(defined?(Union::Foo)).to eq 'constant' 21 | end 22 | end 23 | 24 | context 'factories' do 25 | 26 | specify 'exist for each field' do 27 | expected_fields.each do |field| 28 | expect(struct_class).to respond_to(field) 29 | end 30 | end 31 | 32 | specify 'require a value' do 33 | expected_fields.each do |field| 34 | expect(struct_class.method(field).arity).to eq 1 35 | end 36 | end 37 | 38 | specify 'set the field appropriately' do 39 | clazz = Union.new(:foo, :bar) 40 | obj = clazz.foo(10) 41 | expect(obj.field).to eq :foo 42 | end 43 | 44 | specify 'set the value appropriately' do 45 | clazz = Union.new(:foo, :bar) 46 | obj = clazz.foo(10) 47 | expect(obj.value).to eq 10 48 | end 49 | 50 | specify 'return a frozen union' do 51 | clazz = Union.new(:foo, :bar) 52 | expect(clazz.foo(10)).to be_frozen 53 | end 54 | 55 | specify 'force #new to be private' do 56 | clazz = Union.new(:foo, :bar) 57 | expect { 58 | clazz.new 59 | }.to raise_error(NoMethodError) 60 | end 61 | end 62 | 63 | context 'readers' do 64 | 65 | specify '#field returns the appropriate field' do 66 | clazz = Union.new(:foo, :bar) 67 | expect(clazz.foo(10).field).to eq :foo 68 | end 69 | 70 | specify '#value returns the appropriate field' do 71 | clazz = Union.new(:foo, :bar) 72 | expect(clazz.foo(10).value).to eq 10 73 | end 74 | 75 | specify 'return the appropriate value for the set field' do 76 | clazz = Union.new(:foo, :bar) 77 | expect(clazz.foo(10).foo).to eq 10 78 | end 79 | 80 | specify 'return nil for the unset field' do 81 | clazz = Union.new(:foo, :bar, :baz) 82 | expect(clazz.foo(10).bar).to be_nil 83 | expect(clazz.foo(10).baz).to be_nil 84 | end 85 | end 86 | 87 | context 'predicates' do 88 | 89 | specify 'exist for each field' do 90 | expected_fields.each do |field| 91 | predicate = "#{field}?".to_sym 92 | expect(struct_object).to respond_to(predicate) 93 | expect(struct_object.method(predicate).arity).to eq 0 94 | end 95 | end 96 | 97 | specify 'return true for the set field' do 98 | clazz = Union.new(:foo, :bar) 99 | expect(clazz.foo(10).foo?).to be true 100 | end 101 | 102 | specify 'return false for the unset fields' do 103 | clazz = Union.new(:foo, :bar, :baz) 104 | expect(clazz.foo(10).bar?).to be false 105 | expect(clazz.foo(10).baz?).to be false 106 | end 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /spec/functional/type_check_spec.rb: -------------------------------------------------------------------------------- 1 | module Functional 2 | 3 | describe TypeCheck do 4 | 5 | context 'Type?' do 6 | 7 | it 'returns true when value is of any of the types' do 8 | target = 'foo' 9 | expect(TypeCheck.Type?(target, String, Array, Hash)).to be true 10 | end 11 | 12 | it 'returns false when value is not of any of the types' do 13 | target = 'foo' 14 | expect(TypeCheck.Type?(target, Fixnum, Array, Hash)).to be false 15 | end 16 | end 17 | 18 | context 'Type!' do 19 | 20 | it 'returns the value when value is of any of the types' do 21 | target = 'foo' 22 | expect(TypeCheck.Type!(target, String, Array, Hash)).to be target 23 | end 24 | 25 | it 'raises an exception when value is not of any of the types' do 26 | target = 'foo' 27 | expect { 28 | TypeCheck.Type!(target, Fixnum, Array, Hash) 29 | }.to raise_error(TypeError) 30 | end 31 | end 32 | 33 | context 'Match?' do 34 | 35 | it 'returns true when value is an exact match for at least one of the types' do 36 | target = 'foo' 37 | expect(TypeCheck.Match?(target, String, Array, Hash)).to be true 38 | end 39 | 40 | it 'returns false when value is not an exact match for at least one of the types' do 41 | target = 'foo' 42 | expect(TypeCheck.Match?(target, Fixnum, Array, Hash)).to be false 43 | end 44 | end 45 | 46 | context 'Match!' do 47 | 48 | it 'returns the value when value is an exact match for at least one of the types' do 49 | target = 'foo' 50 | expect(TypeCheck.Match!(target, String, Array, Hash)).to eq target 51 | end 52 | 53 | it 'raises an exception when value is not an exact match for at least one of the types' do 54 | target = 'foo' 55 | expect { 56 | expect(TypeCheck.Match!(target, Fixnum, Array, Hash)).to eq target 57 | }.to raise_error(TypeError) 58 | end 59 | end 60 | 61 | context 'Child?' do 62 | 63 | it 'returns true if value is a class and is also a match or subclass of one of types' do 64 | target = String 65 | expect(TypeCheck.Child?(target, Comparable, Array, Hash)).to be true 66 | end 67 | 68 | it 'returns false if value is not a class' do 69 | target = 'foo' 70 | expect(TypeCheck.Child?(target, Comparable, Array, Hash)).to be false 71 | end 72 | 73 | it 'returns false if value is not a subclass/match for any of the types' do 74 | target = Fixnum 75 | expect(TypeCheck.Child?(target, Symbol, Array, Hash)).to be false 76 | end 77 | end 78 | 79 | context 'Child!' do 80 | 81 | it 'returns the value if value is a class and is also a match or subclass of one of types' do 82 | target = String 83 | expect(TypeCheck.Child!(target, Comparable, Array, Hash)).to eq target 84 | end 85 | 86 | it 'raises an exception if value is not a class' do 87 | target = 'foo' 88 | expect { 89 | TypeCheck.Child!(target, Comparable, Array, Hash) 90 | }.to raise_error(TypeError) 91 | end 92 | 93 | it 'raises an exception if value is not a subclass/match for any of the types' do 94 | target = Fixnum 95 | expect { 96 | TypeCheck.Child!(target, Symbol, Array, Hash) 97 | }.to raise_error(TypeError) 98 | end 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/functional/memo.rb: -------------------------------------------------------------------------------- 1 | require 'functional/synchronization' 2 | 3 | module Functional 4 | 5 | # Memoization is a technique for optimizing functions that are time-consuming 6 | # and/or involve expensive calculations. Every time a memoized function is 7 | # called the result is caches with reference to the given parameters. 8 | # Subsequent calls to the function that use the same parameters will return 9 | # the cached result. As a result the response time for frequently called 10 | # functions is vastly increased (after the first call with any given set of) 11 | # arguments, at the cost of increased memory usage (the cache). 12 | # 13 | # {include:file:doc/memo.md} 14 | # 15 | # @note Memoized method calls are thread safe and can safely be used in 16 | # concurrent systems. Declaring memoization on a function is *not* thread 17 | # safe and should only be done during application initialization. 18 | module Memo 19 | 20 | # @!visibility private 21 | def self.extended(base) 22 | base.extend(ClassMethods) 23 | base.send(:__method_memos__=, {}) 24 | super(base) 25 | end 26 | 27 | # @!visibility private 28 | def self.included(base) 29 | base.extend(ClassMethods) 30 | base.send(:__method_memos__=, {}) 31 | super(base) 32 | end 33 | 34 | # @!visibility private 35 | module ClassMethods 36 | 37 | # @!visibility private 38 | class Memoizer < Synchronization::Object 39 | attr_reader :function, :cache, :max_cache 40 | def initialize(function, max_cache) 41 | super 42 | synchronize do 43 | @function = function 44 | @max_cache = max_cache 45 | @cache = {} 46 | end 47 | end 48 | def max_cache? 49 | max_cache > 0 && cache.size >= max_cache 50 | end 51 | public :synchronize 52 | end 53 | private_constant :Memoizer 54 | 55 | # @!visibility private 56 | attr_accessor :__method_memos__ 57 | 58 | # Returns a memoized version of a referentially transparent function. The 59 | # memoized version of the function keeps a cache of the mapping from 60 | # arguments to results and, when calls with the same arguments are 61 | # repeated often, has higher performance at the expense of higher memory 62 | # use. 63 | # 64 | # @param [Symbol] func the class/module function to memoize 65 | # @param [Hash] opts the options controlling memoization 66 | # @option opts [Fixnum] :at_most the maximum number of memos to store in 67 | # the cache; a value of zero (the default) or `nil` indicates no limit 68 | # 69 | # @raise [ArgumentError] when the method has already been memoized 70 | # @raise [ArgumentError] when :at_most option is a negative number 71 | def memoize(func, opts = {}) 72 | func = func.to_sym 73 | max_cache = opts[:at_most].to_i 74 | raise ArgumentError.new("method :#{func} has already been memoized") if __method_memos__.has_key?(func) 75 | raise ArgumentError.new(':max_cache must be > 0') if max_cache < 0 76 | __method_memos__[func] = Memoizer.new(method(func), max_cache.to_i) 77 | __define_memo_proxy__(func) 78 | nil 79 | end 80 | 81 | # @!visibility private 82 | def __define_memo_proxy__(func) 83 | self.class_eval <<-RUBY 84 | def self.#{func}(*args, &block) 85 | self.__proxy_memoized_method__(:#{func}, *args, &block) 86 | end 87 | RUBY 88 | end 89 | 90 | # @!visibility private 91 | def __proxy_memoized_method__(func, *args, &block) 92 | memo = self.__method_memos__[func] 93 | memo.synchronize do 94 | if block_given? 95 | memo.function.call(*args, &block) 96 | elsif memo.cache.has_key?(args) 97 | memo.cache[args] 98 | else 99 | result = memo.function.call(*args) 100 | memo.cache[args] = result unless memo.max_cache? 101 | end 102 | end 103 | end 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/functional/type_check.rb: -------------------------------------------------------------------------------- 1 | module Functional 2 | 3 | # Supplies type-checking helpers whenever included. 4 | # 5 | # @see http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Actor/TypeCheck.html TypeCheck in Concurrent Ruby 6 | module TypeCheck 7 | 8 | # Performs an `is_a?` check of the given value object against the 9 | # given list of modules and/or classes. 10 | # 11 | # @param [Object] value the object to interrogate 12 | # @param [Module] types zero or more modules and/or classes to check 13 | # the value against 14 | # @return [Boolean] true on success 15 | def Type?(value, *types) 16 | types.any? { |t| value.is_a? t } 17 | end 18 | module_function :Type? 19 | 20 | # Performs an `is_a?` check of the given value object against the 21 | # given list of modules and/or classes. Raises an exception on failure. 22 | # 23 | # @param [Object] value the object to interrogate 24 | # @param [Module] types zero or more modules and/or classes to check 25 | # the value against 26 | # @return [Object] the value object 27 | # 28 | # @raise [Functional::TypeError] when the check fails 29 | def Type!(value, *types) 30 | Type?(value, *types) or 31 | TypeCheck.error(value, 'is not', types) 32 | value 33 | end 34 | module_function :Type! 35 | 36 | # Is the given value object is an instance of or descendant of 37 | # one of the classes/modules in the given list? 38 | # 39 | # Performs the check using the `===` operator. 40 | # 41 | # @param [Object] value the object to interrogate 42 | # @param [Module] types zero or more modules and/or classes to check 43 | # the value against 44 | # @return [Boolean] true on success 45 | def Match?(value, *types) 46 | types.any? { |t| t === value } 47 | end 48 | module_function :Match? 49 | 50 | # Is the given value object is an instance of or descendant of 51 | # one of the classes/modules in the given list? Raises an exception 52 | # on failure. 53 | # 54 | # Performs the check using the `===` operator. 55 | # 56 | # @param [Object] value the object to interrogate 57 | # @param [Module] types zero or more modules and/or classes to check 58 | # the value against 59 | # @return [Object] the value object 60 | # 61 | # @raise [Functional::TypeError] when the check fails 62 | def Match!(value, *types) 63 | Match?(value, *types) or 64 | TypeCheck.error(value, 'is not matching', types) 65 | value 66 | end 67 | module_function :Match! 68 | 69 | # Is the given class a subclass or exact match of one or more 70 | # of the modules and/or classes in the given list? 71 | # 72 | # @param [Class] value the class to interrogate 73 | # @param [Class] types zero or more classes to check the value against 74 | # the value against 75 | # @return [Boolean] true on success 76 | def Child?(value, *types) 77 | Type?(value, Class) && 78 | types.any? { |t| value <= t } 79 | end 80 | module_function :Child? 81 | 82 | # Is the given class a subclass or exact match of one or more 83 | # of the modules and/or classes in the given list? 84 | # 85 | # @param [Class] value the class to interrogate 86 | # @param [Class] types zero or more classes to check the value against 87 | # @return [Class] the value class 88 | # 89 | # @raise [Functional::TypeError] when the check fails 90 | def Child!(value, *types) 91 | Child?(value, *types) or 92 | TypeCheck.error(value, 'is not child', types) 93 | value 94 | end 95 | module_function :Child! 96 | 97 | private 98 | 99 | # Create a {Functional::TypeError} object from the given data. 100 | # 101 | # @param [Object] value the class/method that was being interrogated 102 | # @param [String] message the message fragment to inject into the error 103 | # @param [Object] types list of modules and/or classes that were being 104 | # checked against the value object 105 | # 106 | # @raise [Functional::TypeError] the formatted exception object 107 | def self.error(value, message, types) 108 | raise TypeError, 109 | "Value (#{value.class}) '#{value}' #{message} any of: #{types.join('; ')}." 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/functional/delay.rb: -------------------------------------------------------------------------------- 1 | require 'functional/synchronization' 2 | 3 | module Functional 4 | 5 | # Lazy evaluation of a block yielding an immutable result. Useful for 6 | # expensive operations that may never be needed. 7 | # 8 | # When a `Delay` is created its state is set to `pending`. The value and 9 | # reason are both `nil`. The first time the `#value` method is called the 10 | # enclosed opration will be run and the calling thread will block. Other 11 | # threads attempting to call `#value` will block as well. Once the operation 12 | # is complete the *value* will be set to the result of the operation or the 13 | # *reason* will be set to the raised exception, as appropriate. All threads 14 | # blocked on `#value` will return. Subsequent calls to `#value` will 15 | # immediately return the cached value. The operation will only be run once. 16 | # This means that any side effects created by the operation will only happen 17 | # once as well. 18 | # 19 | # @!macro [new] thread_safe_immutable_object 20 | # 21 | # @note This is a write-once, read-many, thread safe object that can be 22 | # used in concurrent systems. Thread safety guarantees *cannot* be made 23 | # about objects contained *within* this object, however. Ruby variables 24 | # are mutable references to mutable objects. This cannot be changed. The 25 | # best practice it to only encapsulate immutable, frozen, or thread safe 26 | # objects. Ultimately, thread safety is the responsibility of the 27 | # programmer. 28 | # 29 | # @see http://clojuredocs.org/clojure_core/clojure.core/delay Clojure delay 30 | class Delay < Synchronization::Object 31 | 32 | # Create a new `Delay` in the `:pending` state. 33 | # 34 | # @yield the delayed operation to perform 35 | # 36 | # @raise [ArgumentError] if no block is given 37 | def initialize(&block) 38 | raise ArgumentError.new('no block given') unless block_given? 39 | super 40 | synchronize do 41 | @state = :pending 42 | @task = block 43 | end 44 | end 45 | 46 | # Current state of block processing. 47 | # 48 | # @return [Symbol] the current state of block processing 49 | def state 50 | synchronize{ @state } 51 | end 52 | 53 | # The exception raised when processing the block. Returns `nil` if the 54 | # operation is still `:pending` or has been `:fulfilled`. 55 | # 56 | # @return [StandardError] the exception raised when processing the block 57 | # else nil. 58 | def reason 59 | synchronize{ @reason } 60 | end 61 | 62 | # Return the (possibly memoized) value of the delayed operation. 63 | # 64 | # If the state is `:pending` then the calling thread will block while the 65 | # operation is performed. All other threads simultaneously calling `#value` 66 | # will block as well. Once the operation is complete (either `:fulfilled` or 67 | # `:rejected`) all waiting threads will unblock and the new value will be 68 | # returned. 69 | # 70 | # If the state is not `:pending` when `#value` is called the (possibly 71 | # memoized) value will be returned without blocking and without performing 72 | # the operation again. 73 | # 74 | # @return [Object] the (possibly memoized) result of the block operation 75 | def value 76 | synchronize{ execute_task_once } 77 | end 78 | 79 | # Has the delay been fulfilled? 80 | # @return [Boolean] 81 | def fulfilled? 82 | synchronize{ @state == :fulfilled } 83 | end 84 | alias_method :value?, :fulfilled? 85 | 86 | # Has the delay been rejected? 87 | # @return [Boolean] 88 | def rejected? 89 | synchronize{ @state == :rejected } 90 | end 91 | alias_method :reason?, :rejected? 92 | 93 | # Is delay completion still pending? 94 | # @return [Boolean] 95 | def pending? 96 | synchronize{ @state == :pending } 97 | end 98 | 99 | protected 100 | 101 | # @!visibility private 102 | # 103 | # Execute the enclosed task then cache and return the result if the current 104 | # state is pending. Otherwise, return the cached result. 105 | # 106 | # @return [Object] the result of the block operation 107 | def execute_task_once 108 | if @state == :pending 109 | begin 110 | @value = @task.call 111 | @state = :fulfilled 112 | rescue => ex 113 | @reason = ex 114 | @state = :rejected 115 | end 116 | end 117 | @value 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /spec/functional/delay_spec.rb: -------------------------------------------------------------------------------- 1 | module Functional 2 | 3 | describe Delay do 4 | 5 | let!(:fulfilled_value) { 10 } 6 | let!(:rejected_reason) { StandardError.new('mojo jojo') } 7 | 8 | let(:pending_subject) do 9 | Delay.new{ fulfilled_value } 10 | end 11 | 12 | let(:fulfilled_subject) do 13 | delay = Delay.new{ fulfilled_value } 14 | delay.tap{ delay.value } 15 | end 16 | 17 | let(:rejected_subject) do 18 | delay = Delay.new{ raise rejected_reason } 19 | delay.tap{ delay.value } 20 | end 21 | 22 | specify{ Functional::Protocol::Satisfy! Delay, :Disposition } 23 | 24 | context '#initialize' do 25 | 26 | it 'sets the state to :pending' do 27 | expect(Delay.new{ nil }.state).to eq :pending 28 | expect(Delay.new{ nil }).to be_pending 29 | end 30 | 31 | it 'raises an exception when no block given' do 32 | expect { 33 | Delay.new 34 | }.to raise_error(ArgumentError) 35 | end 36 | end 37 | 38 | context '#state' do 39 | 40 | it 'is :pending when first created' do 41 | f = pending_subject 42 | expect(f.state).to eq(:pending) 43 | expect(f).to be_pending 44 | end 45 | 46 | it 'is :fulfilled when the handler completes' do 47 | f = fulfilled_subject 48 | expect(f.state).to eq(:fulfilled) 49 | expect(f).to be_fulfilled 50 | end 51 | 52 | it 'is :rejected when the handler raises an exception' do 53 | f = rejected_subject 54 | expect(f.state).to eq(:rejected) 55 | expect(f).to be_rejected 56 | end 57 | end 58 | 59 | context '#value' do 60 | 61 | let(:task){ proc{ nil } } 62 | 63 | it 'blocks the caller when :pending and timeout is nil' do 64 | f = pending_subject 65 | expect(f.value).to be_truthy 66 | expect(f).to be_fulfilled 67 | end 68 | 69 | it 'is nil when :rejected' do 70 | expected = rejected_subject.value 71 | expect(expected).to be_nil 72 | end 73 | 74 | it 'is set to the return value of the block when :fulfilled' do 75 | expected = fulfilled_subject.value 76 | expect(expected).to eq fulfilled_value 77 | end 78 | 79 | it 'does not call the block before #value is called' do 80 | expect(task).not_to receive(:call).with(any_args) 81 | Delay.new(&task) 82 | end 83 | 84 | it 'calls the block when #value is called' do 85 | expect(task).to receive(:call).once.with(any_args).and_return(nil) 86 | Delay.new(&task).value 87 | end 88 | 89 | it 'only calls the block once no matter how often #value is called' do 90 | expect(task).to receive(:call).once.with(any_args).and_return(nil) 91 | delay = Delay.new(&task) 92 | 5.times{ delay.value } 93 | end 94 | end 95 | 96 | context '#reason' do 97 | 98 | it 'is nil when :pending' do 99 | expect(pending_subject.reason).to be_nil 100 | end 101 | 102 | it 'is nil when :fulfilled' do 103 | expect(fulfilled_subject.reason).to be_nil 104 | end 105 | 106 | it 'is set to error object of the exception when :rejected' do 107 | expect(rejected_subject.reason).to be_a(Exception) 108 | expect(rejected_subject.reason.to_s).to match(/#{rejected_reason}/) 109 | end 110 | end 111 | 112 | context 'predicates' do 113 | 114 | specify '#value? returns true when :fulfilled' do 115 | expect(pending_subject).to_not be_value 116 | expect(fulfilled_subject).to be_value 117 | expect(rejected_subject).to_not be_value 118 | end 119 | 120 | specify '#reason? returns true when :rejected' do 121 | expect(pending_subject).to_not be_reason 122 | expect(fulfilled_subject).to_not be_reason 123 | expect(rejected_subject).to be_reason 124 | end 125 | 126 | specify '#fulfilled? returns true when :fulfilled' do 127 | expect(pending_subject).to_not be_fulfilled 128 | expect(fulfilled_subject).to be_fulfilled 129 | expect(rejected_subject).to_not be_fulfilled 130 | end 131 | 132 | specify '#rejected? returns true when :rejected' do 133 | expect(pending_subject).to_not be_rejected 134 | expect(fulfilled_subject).to_not be_rejected 135 | expect(rejected_subject).to be_rejected 136 | end 137 | 138 | specify '#pending? returns true when :pending' do 139 | expect(pending_subject).to be_pending 140 | expect(fulfilled_subject).to_not be_pending 141 | expect(rejected_subject).to_not be_pending 142 | end 143 | end 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /spec/functional/abstract_struct_shared.rb: -------------------------------------------------------------------------------- 1 | shared_examples :abstract_struct do 2 | 3 | specify { Functional::Protocol::Satisfy! struct_class, :Struct } 4 | 5 | let(:other_struct) do 6 | Class.new do 7 | include Functional::AbstractStruct 8 | self.fields = [:foo, :bar, :baz].freeze 9 | self.datatype = :other_struct 10 | end 11 | end 12 | 13 | context 'field collection' do 14 | 15 | it 'contains all possible fields' do 16 | expected_fields.each do |field| 17 | expect(struct_class.fields).to include(field) 18 | end 19 | end 20 | 21 | it 'is frozen' do 22 | expect(struct_class.fields).to be_frozen 23 | end 24 | 25 | it 'does not overwrite fields for other structs' do 26 | expect(struct_class.fields).to_not eq other_struct.fields 27 | end 28 | 29 | it 'is the same when called on the class and on an object' do 30 | expect(struct_class.fields).to eq struct_object.fields 31 | end 32 | end 33 | 34 | context 'readers' do 35 | 36 | specify '#values returns all values in an array' do 37 | expect(struct_object.values).to eq expected_values 38 | end 39 | 40 | specify '#values is frozen' do 41 | expect(struct_object.values).to be_frozen 42 | end 43 | 44 | specify 'exist for each field' do 45 | expected_fields.each do |field| 46 | expect(struct_object).to respond_to(field) 47 | expect(struct_object.method(field).arity).to eq 0 48 | end 49 | end 50 | 51 | specify 'return the appropriate value all fields' do 52 | expected_fields.each_with_index do |field, i| 53 | expect(struct_object.send(field)).to eq expected_values[i] 54 | end 55 | end 56 | end 57 | 58 | context 'enumeration' do 59 | 60 | specify '#each_pair with a block iterates over all fields and values' do 61 | fields = [] 62 | values = [] 63 | 64 | struct_object.each_pair do |field, value| 65 | fields << field 66 | values << value 67 | end 68 | 69 | expect(fields).to eq struct_object.fields 70 | expect(values).to eq struct_object.values 71 | end 72 | 73 | specify '#each_pair without a block returns an Enumerable' do 74 | expect(struct_object.each_pair).to be_a Enumerable 75 | end 76 | 77 | specify '#each with a block iterates over all values' do 78 | values = [] 79 | 80 | struct_object.each do |value| 81 | values << value 82 | end 83 | 84 | expect(values).to eq struct_object.values 85 | end 86 | 87 | specify '#each without a block returns an Enumerable' do 88 | expect(struct_object.each).to be_a Enumerable 89 | end 90 | end 91 | 92 | context 'reflection' do 93 | 94 | specify 'always creates frozen objects' do 95 | expect(struct_object).to be_frozen 96 | end 97 | 98 | specify 'asserts equality for two structs of the same class with equal values' do 99 | other = struct_object.dup 100 | 101 | expect(struct_object).to eq other 102 | expect(struct_object).to eql other 103 | end 104 | 105 | specify 'rejects equality for two structs of different classes' do 106 | other = Struct.new(*expected_fields).new(*expected_values) 107 | 108 | expect(struct_object).to_not eq other 109 | expect(struct_object).to_not eql other 110 | end 111 | 112 | specify 'rejects equality for two structs of the same class with different values' do 113 | expect(struct_object).to_not eq other_object 114 | expect(struct_object).to_not eql other_struct 115 | end 116 | 117 | specify '#to_h returns a Hash with all field/value pairs' do 118 | hsh = struct_object.to_h 119 | 120 | expect(hsh.keys).to eq struct_object.fields 121 | expect(hsh.values).to eq struct_object.values 122 | end 123 | 124 | specify '#inspect result is enclosed in brackets' do 125 | expect(struct_object.inspect).to match(/^#$/) 127 | end 128 | 129 | specify '#inspect result has lowercase class name as first element' do 130 | struct = described_class.to_s.split('::').last.downcase 131 | expect(struct_object.inspect).to match(/^#<#{struct} /) 132 | end 133 | 134 | specify '#inspect includes all field/value pairs' do 135 | struct_object.fields.each_with_index do |field, i| 136 | value_regex = "\"?#{struct_object.values[i]}\"?" 137 | expect(struct_object.inspect).to match(/:#{field}=>#{value_regex}/) 138 | end 139 | end 140 | 141 | specify '#inspect is aliased as #to_s' do 142 | expect(struct_object.inspect).to eq struct_object.to_s 143 | end 144 | 145 | specify '#length returns the number of fields' do 146 | expect(struct_object.length).to eq struct_class.fields.length 147 | expect(struct_object.length).to eq expected_fields.length 148 | end 149 | 150 | specify 'aliases #length as #size' do 151 | expect(struct_object.length).to eq struct_object.size 152 | end 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /spec/functional/final_var_spec.rb: -------------------------------------------------------------------------------- 1 | module Functional 2 | 3 | describe FinalVar do 4 | 5 | context 'instanciation' do 6 | 7 | it 'is unset when no arguments given' do 8 | expect(FinalVar.new).to_not be_set 9 | end 10 | 11 | it 'is set with the given argument' do 12 | expect(FinalVar.new(41)).to be_set 13 | end 14 | end 15 | 16 | context '#get' do 17 | 18 | subject { FinalVar.new } 19 | 20 | it 'returns nil when unset' do 21 | expect(subject.get).to be nil 22 | end 23 | 24 | it 'returns the value when set' do 25 | expect(FinalVar.new(42).get).to eq 42 26 | end 27 | 28 | it 'is aliased as #value' do 29 | expect(subject.value).to be nil 30 | subject.set(42) 31 | expect(subject.value).to eq 42 32 | end 33 | end 34 | 35 | context '#set' do 36 | 37 | subject { FinalVar.new } 38 | 39 | it 'sets the value when unset' do 40 | subject.set(42) 41 | expect(subject.get).to eq 42 42 | end 43 | 44 | it 'returns the new value when unset' do 45 | expect(subject.set(42)).to eq 42 46 | end 47 | 48 | it 'raises an exception when already set' do 49 | subject.set(42) 50 | expect { 51 | subject.set(42) 52 | }.to raise_error(Functional::FinalityError) 53 | end 54 | 55 | it 'is aliased as #value=' do 56 | subject.value = 42 57 | expect(subject.get).to eq 42 58 | end 59 | end 60 | 61 | context '#set?' do 62 | 63 | it 'returns false when unset' do 64 | expect(FinalVar.new).to_not be_set 65 | end 66 | 67 | it 'returns true when set' do 68 | expect(FinalVar.new(42)).to be_set 69 | end 70 | 71 | it 'is aliased as value?' do 72 | expect(FinalVar.new.value?).to be false 73 | expect(FinalVar.new(42).value?).to be true 74 | end 75 | end 76 | 77 | context '#get_or_set' do 78 | 79 | it 'sets the value when unset' do 80 | subject = FinalVar.new 81 | subject.get_or_set(42) 82 | expect(subject.get).to eq 42 83 | end 84 | 85 | it 'returns the new value when previously unset' do 86 | subject = FinalVar.new 87 | expect(subject.get_or_set(42)).to eq 42 88 | end 89 | 90 | it 'returns the current value when already set' do 91 | subject = FinalVar.new(100) 92 | expect(subject.get_or_set(42)).to eq 100 93 | end 94 | end 95 | 96 | context '#fetch' do 97 | 98 | it 'returns the given default value when unset' do 99 | subject = FinalVar.new 100 | expect(subject.fetch(42)).to eq 42 101 | end 102 | 103 | it 'does not change the current value when unset' do 104 | subject = FinalVar.new 105 | subject.fetch(42) 106 | expect(subject.get).to be nil 107 | end 108 | 109 | it 'returns the current value when already set' do 110 | subject = FinalVar.new(100) 111 | expect(subject.get_or_set(42)).to eq 100 112 | end 113 | end 114 | 115 | context 'reflection' do 116 | 117 | specify '#eql? returns false when unset' do 118 | expect(FinalVar.new.eql?(nil)).to be false 119 | expect(FinalVar.new.eql?(42)).to be false 120 | expect(FinalVar.new.eql?(FinalVar.new.value)).to be false 121 | end 122 | 123 | specify '#eql? returns false when set and the value does not match other' do 124 | subject = FinalVar.new(42) 125 | expect(subject.eql?(100)).to be false 126 | end 127 | 128 | specify '#eql? returns true when set and the value matches other' do 129 | subject = FinalVar.new(42) 130 | expect(subject.eql?(42)).to be true 131 | end 132 | 133 | specify '#eql? returns true when set and other is a FinalVar with the same value' do 134 | subject = FinalVar.new(42) 135 | other = FinalVar.new(42) 136 | expect(subject.eql?(other)).to be true 137 | end 138 | 139 | specify 'aliases #== as #eql?' do 140 | expect(FinalVar.new == nil).to be false 141 | expect(FinalVar.new == 42).to be false 142 | expect(FinalVar.new == FinalVar.new).to be false 143 | expect(FinalVar.new(42) == 42).to be true 144 | expect(FinalVar.new(42) == FinalVar.new(42)).to be true 145 | end 146 | 147 | specify '#inspect includes the word "value" and the value when set' do 148 | subject = FinalVar.new(42) 149 | expect(subject.inspect).to match(/value\s?=\s?42\s*>$/) 150 | end 151 | 152 | specify '#inspect include the word "unset" when unset' do 153 | subject = FinalVar.new 154 | expect(subject.inspect).to match(/unset\s*>$/i) 155 | end 156 | 157 | specify '#to_s returns nil as a string when unset' do 158 | expect(FinalVar.new.to_s).to eq nil.to_s 159 | end 160 | 161 | specify '#to_s returns the value as a string when set' do 162 | expect(FinalVar.new(42).to_s).to eq 42.to_s 163 | expect(FinalVar.new('42').to_s).to eq '42' 164 | end 165 | end 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /lib/functional/abstract_struct.rb: -------------------------------------------------------------------------------- 1 | require 'functional/protocol' 2 | require 'functional/synchronization' 3 | 4 | Functional::SpecifyProtocol(:Struct) do 5 | instance_method :fields 6 | instance_method :values 7 | instance_method :length 8 | instance_method :each 9 | instance_method :each_pair 10 | end 11 | 12 | module Functional 13 | 14 | # An abstract base class for immutable struct classes. 15 | # @!visibility private 16 | module AbstractStruct 17 | 18 | # @return [Array] the values of all record fields in order, frozen 19 | attr_reader :values 20 | 21 | # Yields the value of each record field in order. 22 | # If no block is given an enumerator is returned. 23 | # 24 | # @yieldparam [Object] value the value of the given field 25 | # 26 | # @return [Enumerable] when no block is given 27 | def each 28 | return enum_for(:each) unless block_given? 29 | fields.each do |field| 30 | yield(self.send(field)) 31 | end 32 | end 33 | 34 | # Yields the name and value of each record field in order. 35 | # If no block is given an enumerator is returned. 36 | # 37 | # @yieldparam [Symbol] field the record field for the current iteration 38 | # @yieldparam [Object] value the value of the current field 39 | # 40 | # @return [Enumerable] when no block is given 41 | def each_pair 42 | return enum_for(:each_pair) unless block_given? 43 | fields.each do |field| 44 | yield(field, self.send(field)) 45 | end 46 | end 47 | 48 | # Equality--Returns `true` if `other` has the same record subclass and has equal 49 | # field values (according to `Object#==`). 50 | # 51 | # @param [Object] other the other record to compare for equality 52 | # @return [Boolean] true when equal else false 53 | def eql?(other) 54 | self.class == other.class && self.to_h == other.to_h 55 | end 56 | alias_method :==, :eql? 57 | 58 | # @!macro [attach] inspect_method 59 | # 60 | # Describe the contents of this struct in a string. Will include the name of the 61 | # record class, all fields, and all values. 62 | # 63 | # @return [String] the class and contents of this record 64 | def inspect 65 | state = to_h.to_s.gsub(/^{/, '').gsub(/}$/, '') 66 | "#<#{self.class.datatype} #{self.class} #{state}>" 67 | end 68 | alias_method :to_s, :inspect 69 | 70 | # Returns the number of record fields. 71 | # 72 | # @return [Fixnum] the number of record fields 73 | def length 74 | fields.length 75 | end 76 | alias_method :size, :length 77 | 78 | # A frozen array of all record fields. 79 | # 80 | # @return [Array] all record fields in order, frozen 81 | def fields 82 | self.class.fields 83 | end 84 | 85 | # Returns a Hash containing the names and values for the record’s fields. 86 | # 87 | # @return [Hash] collection of all fields and their associated values 88 | def to_h 89 | @data 90 | end 91 | 92 | protected 93 | 94 | # Set the internal data hash to a copy of the given hash and freeze it. 95 | # @param [Hash] data the data hash 96 | # 97 | # @!visibility private 98 | def set_data_hash(data) 99 | @data = data.dup.freeze 100 | end 101 | 102 | # Set the internal values array to a copy of the given array and freeze it. 103 | # @param [Array] values the values array 104 | # 105 | # @!visibility private 106 | def set_values_array(values) 107 | @values = values.dup.freeze 108 | end 109 | 110 | # Define a new struct class and, if necessary, register it with 111 | # the calling class/module. Will also set the datatype and fields 112 | # class attributes on the new struct class. 113 | # 114 | # @param [Module] parent the class/module that is defining the new struct 115 | # @param [Symbol] datatype the datatype value for the new struct class 116 | # @param [Array] fields the list of symbolic names for all data fields 117 | # @return [Functional::AbstractStruct, Array] the new class and the 118 | # (possibly) updated fields array 119 | # 120 | # @!visibility private 121 | def self.define_class(parent, datatype, fields) 122 | struct = Class.new(Functional::Synchronization::Object){ include AbstractStruct } 123 | if fields.first.is_a? String 124 | parent.const_set(fields.first, struct) 125 | fields = fields[1, fields.length-1] 126 | end 127 | fields = fields.collect{|field| field.to_sym }.freeze 128 | struct.send(:datatype=, datatype.to_sym) 129 | struct.send(:fields=, fields) 130 | [struct, fields] 131 | end 132 | 133 | private 134 | 135 | def self.included(base) 136 | base.extend(ClassMethods) 137 | super(base) 138 | end 139 | 140 | # Class methods added to a class that includes {Functional::PatternMatching} 141 | # 142 | # @!visibility private 143 | module ClassMethods 144 | 145 | # A frozen Array of all record fields in order 146 | attr_reader :fields 147 | 148 | # A symbol describing the object's datatype 149 | attr_reader :datatype 150 | 151 | private 152 | 153 | # A frozen Array of all record fields in order 154 | attr_writer :fields 155 | 156 | # A symbol describing the object's datatype 157 | attr_writer :datatype 158 | 159 | fields = [].freeze 160 | datatype = :struct 161 | end 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /lib/functional/value_struct.rb: -------------------------------------------------------------------------------- 1 | require 'functional/synchronization' 2 | 3 | module Functional 4 | 5 | # A variation on Ruby's `OpenStruct` in which all fields are immutable and 6 | # set at instantiation. For compatibility with {Functional::FinalStruct}, 7 | # predicate methods exist for all potential fields and these predicates 8 | # indicate if the field has been set. Calling a predicate method for a field 9 | # that does not exist on the struct will return false. 10 | # 11 | # Unlike {Functional::Record}, which returns a new class which can be used to 12 | # create immutable objects, `ValueStruct` creates simple immutable objects. 13 | # 14 | # @example Instanciation 15 | # name = Functional::ValueStruct.new(first: 'Douglas', last: 'Adams') 16 | # 17 | # name.first #=> 'Douglas' 18 | # name.last #=> 'Adams' 19 | # name.first? #=> true 20 | # name.last? #=> true 21 | # name.middle? #=> false 22 | # 23 | # @see Functional::Record 24 | # @see Functional::FinalStruct 25 | # @see http://www.ruby-doc.org/stdlib-2.1.2/libdoc/ostruct/rdoc/OpenStruct.html 26 | # 27 | # @!macro thread_safe_immutable_object 28 | class ValueStruct < Synchronization::Object 29 | 30 | def initialize(attributes) 31 | raise ArgumentError.new('attributes must be given as a hash') unless attributes.respond_to?(:each_pair) 32 | super 33 | @attribute_hash = {} 34 | attributes.each_pair do |field, value| 35 | set_attribute(field, value) 36 | end 37 | @attribute_hash.freeze 38 | ensure_ivar_visibility! 39 | self.freeze 40 | end 41 | 42 | # Get the value of the given field. 43 | # 44 | # @param [Symbol] field the field to retrieve the value for 45 | # @return [Object] the value of the field is set else nil 46 | def get(field) 47 | @attribute_hash[field.to_sym] 48 | end 49 | alias_method :[], :get 50 | 51 | # Check the internal hash to unambiguously verify that the given 52 | # attribute has been set. 53 | # 54 | # @param [Symbol] field the field to get the value for 55 | # @return [Boolean] true if the field has been set else false 56 | def set?(field) 57 | @attribute_hash.has_key?(field.to_sym) 58 | end 59 | 60 | # Get the current value of the given field if already set else return the given 61 | # default value. 62 | # 63 | # @param [Symbol] field the field to get the value for 64 | # @param [Object] default the value to return if the field has not been set 65 | # @return [Object] the value of the given field else the given default value 66 | def fetch(field, default) 67 | @attribute_hash.fetch(field.to_sym, default) 68 | end 69 | 70 | # Calls the block once for each attribute, passing the key/value pair as parameters. 71 | # If no block is given, an enumerator is returned instead. 72 | # 73 | # @yieldparam [Symbol] field the struct field for the current iteration 74 | # @yieldparam [Object] value the value of the current field 75 | # 76 | # @return [Enumerable] when no block is given 77 | def each_pair 78 | return enum_for(:each_pair) unless block_given? 79 | @attribute_hash.each do |field, value| 80 | yield(field, value) 81 | end 82 | end 83 | 84 | # Converts the `ValueStruct` to a `Hash` with keys representing each attribute 85 | # (as symbols) and their corresponding values. 86 | # 87 | # @return [Hash] a `Hash` representing this struct 88 | def to_h 89 | @attribute_hash.dup # dup removes the frozen flag 90 | end 91 | 92 | # Compares this object and other for equality. A `ValueStruct` is `eql?` to 93 | # other when other is a `ValueStruct` and the two objects have identical 94 | # fields and values. 95 | # 96 | # @param [Object] other the other record to compare for equality 97 | # @return [Boolean] true when equal else false 98 | def eql?(other) 99 | other.is_a?(self.class) && @attribute_hash == other.to_h 100 | end 101 | alias_method :==, :eql? 102 | 103 | # Describe the contents of this object in a string. 104 | # 105 | # @return [String] the string representation of this object 106 | # 107 | # @!visibility private 108 | def inspect 109 | state = @attribute_hash.to_s.gsub(/^{/, '').gsub(/}$/, '') 110 | "#<#{self.class} #{state}>" 111 | end 112 | alias_method :to_s, :inspect 113 | 114 | protected 115 | 116 | # Set the value of the give field to the given value. 117 | # 118 | # @param [Symbol] field the field to set the value for 119 | # @param [Object] value the value to set the field to 120 | # @return [Object] the final value of the given field 121 | # 122 | # @!visibility private 123 | def set_attribute(field, value) 124 | @attribute_hash[field.to_sym] = value 125 | end 126 | 127 | # Check the method name and args for signatures matching potential 128 | # final predicate methods. If the signature matches call the appropriate 129 | # method 130 | # 131 | # @param [Symbol] symbol the name of the called function 132 | # @param [Array] args zero or more arguments 133 | # @return [Object] the result of the proxied method or the `super` call 134 | # 135 | # @!visibility private 136 | def method_missing(symbol, *args) 137 | if args.length == 0 && (match = /([^\?]+)\?$/.match(symbol)) 138 | set?(match[1]) 139 | elsif args.length == 0 && set?(symbol) 140 | get(symbol) 141 | else 142 | super 143 | end 144 | end 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /lib/functional/final_var.rb: -------------------------------------------------------------------------------- 1 | require 'functional/synchronization' 2 | 3 | module Functional 4 | 5 | # An exception raised when an attempt is made to modify an 6 | # immutable object or attribute. 7 | FinalityError = Class.new(StandardError) 8 | 9 | # A thread safe object that holds a single value and is "final" (meaning 10 | # that the value can be set at most once after which it becomes immutable). 11 | # The value can be set at instantiation which will result in the object 12 | # becoming fully and immediately immutable. Attempting to set the value 13 | # once it has been set is a logical error and will result in an exception 14 | # being raised. 15 | # 16 | # @example Instanciation With No Value 17 | # f = Functional::FinalVar.new 18 | # #=> # 19 | # f.set? #=> false 20 | # f.value #=> nil 21 | # f.value = 42 #=> 42 22 | # f.inspect 23 | # #=> "#" 24 | # f.set? #=> true 25 | # f.value #=> 42 26 | # 27 | # @example Instanciation With an Initial Value 28 | # f = Functional::FinalVar.new(42) 29 | # #=> # 30 | # f.set? #=> true 31 | # f.value #=> 42 32 | # 33 | # @see Functional::FinalStruct 34 | # @see http://en.wikipedia.org/wiki/Final_(Java) Java `final` keyword 35 | # 36 | # @!macro [new] thread_safe_final_object 37 | # 38 | # @note This is a write-once, read-many, thread safe object that can 39 | # be used in concurrent systems. Thread safety guarantees *cannot* be made 40 | # about objects contained *within* this object, however. Ruby variables are 41 | # mutable references to mutable objects. This cannot be changed. The best 42 | # practice it to only encapsulate immutable, frozen, or thread safe objects. 43 | # Ultimately, thread safety is the responsibility of the programmer. 44 | class FinalVar < Synchronization::Object 45 | 46 | # @!visibility private 47 | NO_VALUE = Object.new.freeze 48 | 49 | # Create a new `FinalVar` with the given value or "unset" when 50 | # no value is given. 51 | # 52 | # @param [Object] value if given, the immutable value of the object 53 | def initialize(value = NO_VALUE) 54 | super 55 | synchronize{ @value = value } 56 | end 57 | 58 | # Get the current value or nil if unset. 59 | # 60 | # @return [Object] the current value or nil 61 | def get 62 | synchronize { has_been_set? ? @value : nil } 63 | end 64 | alias_method :value, :get 65 | 66 | # Set the value. Will raise an exception if already set. 67 | # 68 | # @param [Object] value the value to set 69 | # @return [Object] the new value 70 | # @raise [Functional::FinalityError] if the value has already been set 71 | def set(value) 72 | synchronize do 73 | if has_been_set? 74 | raise FinalityError.new('value has already been set') 75 | else 76 | @value = value 77 | end 78 | end 79 | end 80 | alias_method :value=, :set 81 | 82 | # Has the value been set? 83 | # 84 | # @return [Boolean] true when the value has been set else false 85 | def set? 86 | synchronize { has_been_set? } 87 | end 88 | alias_method :value?, :set? 89 | 90 | # Get the value if it has been set else set the value. 91 | # 92 | # @param [Object] value the value to set 93 | # @return [Object] the current value if already set else the new value 94 | def get_or_set(value) 95 | synchronize do 96 | if has_been_set? 97 | @value 98 | else 99 | @value = value 100 | end 101 | end 102 | end 103 | 104 | # Get the value if set else return the given default value. 105 | # 106 | # @param [Object] default the value to return if currently unset 107 | # @return [Object] the current value when set else the given default 108 | def fetch(default) 109 | synchronize { has_been_set? ? @value : default } 110 | end 111 | 112 | # Compares this object and other for equality. A `FinalVar` that is unset 113 | # is never equal to anything else (it represents a complete absence of value). 114 | # When set a `FinalVar` is equal to another `FinalVar` if they have the same 115 | # value. A `FinalVar` is equal to another object if its value is equal to 116 | # the other object using Ruby's normal equality rules. 117 | # 118 | # @param [Object] other the object to compare equality to 119 | # @return [Boolean] true if equal else false 120 | def eql?(other) 121 | if (val = fetch(NO_VALUE)) == NO_VALUE 122 | false 123 | elsif other.is_a?(FinalVar) 124 | val == other.value 125 | else 126 | val == other 127 | end 128 | end 129 | alias_method :==, :eql? 130 | 131 | # Describe the contents of this object in a string. 132 | # 133 | # @return [String] the string representation of this object 134 | # 135 | # @!visibility private 136 | def inspect 137 | if (val = fetch(NO_VALUE)) == NO_VALUE 138 | val = 'unset' 139 | else 140 | val = "value=#{val.is_a?(String) ? ('"' + val + '"') : val }" 141 | end 142 | "#<#{self.class} #{val}>" 143 | end 144 | 145 | # Describe the contents of this object in a string. 146 | # 147 | # @return [String] the string representation of this object 148 | # 149 | # @!visibility private 150 | def to_s 151 | value.to_s 152 | end 153 | 154 | private 155 | 156 | # Checks the set status without locking the mutex. 157 | # @return [Boolean] true when set else false 158 | def has_been_set? 159 | @value != NO_VALUE 160 | end 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /spec/functional/memo_spec.rb: -------------------------------------------------------------------------------- 1 | module Functional 2 | 3 | describe Memo do 4 | 5 | def create_new_memo_class 6 | Class.new do 7 | include Functional::Memo 8 | 9 | class << self 10 | attr_accessor :count 11 | end 12 | 13 | self.count = 0 14 | 15 | def self.add(a, b) 16 | self.count += 1 17 | a + b 18 | end 19 | memoize :add 20 | 21 | def self.increment(n) 22 | self.count += 1 23 | end 24 | 25 | def self.exception(ex = StandardError) 26 | raise ex 27 | end 28 | end 29 | end 30 | 31 | subject{ create_new_memo_class } 32 | 33 | context 'specification' do 34 | 35 | it 'raises an exception when the method is not defined' do 36 | expect { 37 | subject.memoize(:bogus) 38 | }.to raise_error(NameError) 39 | end 40 | 41 | it 'raises an exception when the given method has already been memoized' do 42 | expect{ 43 | subject.memoize(:add) 44 | }.to raise_error(ArgumentError) 45 | end 46 | 47 | it 'allocates a different cache for each class/module' do 48 | class_1 = create_new_memo_class 49 | class_2 = create_new_memo_class 50 | 51 | 10.times do 52 | class_1.add(0, 0) 53 | class_2.add(0, 0) 54 | end 55 | 56 | expect(class_1.count).to eq 1 57 | expect(class_2.count).to eq 1 58 | end 59 | 60 | it 'works when included in a class' do 61 | subject = Class.new do 62 | include Functional::Memo 63 | class << self 64 | attr_accessor :count 65 | end 66 | self.count = 0 67 | def self.foo 68 | self.count += 1 69 | end 70 | memoize :foo 71 | end 72 | 73 | 10.times{ subject.foo } 74 | expect(subject.count).to eq 1 75 | end 76 | 77 | it 'works when included in a module' do 78 | subject = Module.new do 79 | include Functional::Memo 80 | class << self 81 | attr_accessor :count 82 | end 83 | self.count = 0 84 | def self.foo 85 | self.count += 1 86 | end 87 | memoize :foo 88 | end 89 | 90 | 10.times{ subject.foo } 91 | expect(subject.count).to eq 1 92 | end 93 | 94 | it 'works when extended by a module' do 95 | subject = Module.new do 96 | extend Functional::Memo 97 | class << self 98 | attr_accessor :count 99 | end 100 | self.count = 0 101 | def self.foo 102 | self.count += 1 103 | end 104 | memoize :foo 105 | end 106 | 107 | 10.times{ subject.foo } 108 | expect(subject.count).to eq 1 109 | end 110 | end 111 | 112 | context 'caching behavior' do 113 | 114 | it 'calls the real method on first instance of given args' do 115 | subject.add(1, 2) 116 | expect(subject.count).to eq 1 117 | end 118 | 119 | it 'calls the real method on first instance of given args' do 120 | subject.add(1, 2) 121 | expect(subject.count).to eq 1 122 | end 123 | 124 | it 'uses the memo on second instance of given args' do 125 | 5.times { subject.add(1, 2) } 126 | expect(subject.count).to eq 1 127 | end 128 | 129 | it 'calls the real method when given a block' do 130 | 5.times { subject.add(1, 2){ nil } } 131 | expect(subject.count).to eq 5 132 | end 133 | 134 | it 'raises an exception when arity does not match' do 135 | expect { 136 | subject.add 137 | }.to raise_error(ArgumentError) 138 | end 139 | end 140 | 141 | context 'maximum cache size' do 142 | 143 | it 'raises an exception when given a non-positive :at_most' do 144 | expect { 145 | subject.memoize(:increment, at_most: -1) 146 | }.to raise_error(ArgumentError) 147 | end 148 | 149 | it 'sets no limit when :at_most not given' do 150 | subject.memoize(:increment) 151 | 10000.times{|i| subject.increment(i) } 152 | expect(subject.count).to eq 10000 153 | end 154 | 155 | it 'calls the real method when the :at_most size is reached' do 156 | subject.memoize(:increment, at_most: 5) 157 | 10000.times{|i| subject.increment(i % 10) } 158 | expect(subject.count).to eq 5005 159 | end 160 | end 161 | 162 | context 'thread safety' do 163 | 164 | let(:memoizer_factory){ Functional::Memo::ClassMethods.const_get(:Memoizer) } 165 | let(:memoizer){ memoizer_factory.new(:func, 0) } 166 | 167 | before(:each) do 168 | allow(memoizer_factory).to receive(:new).with(any_args).and_return(memoizer) 169 | end 170 | 171 | it 'locks a mutex whenever a memoized function is called' do 172 | expect(memoizer).to receive(:synchronize).exactly(:once).with(no_args) 173 | 174 | subject.memoize(:increment) 175 | subject.increment(0) 176 | end 177 | 178 | it 'unlocks the mutex whenever a memoized function is called' do 179 | expect(memoizer).to receive(:synchronize).exactly(:once).with(no_args) 180 | 181 | subject.memoize(:increment) 182 | subject.increment(0) 183 | end 184 | 185 | it 'unlocks the mutex when the method call raises an exception' do 186 | expect(memoizer).to receive(:synchronize).exactly(:once).with(no_args) 187 | 188 | subject.memoize(:exception) 189 | begin 190 | subject.exception 191 | rescue 192 | # suppress 193 | end 194 | end 195 | 196 | it 'uses different mutexes for different functions' do 197 | expect(memoizer_factory).to receive(:new).with(any_args).exactly(3).times.and_return(memoizer) 198 | # once for memoize(:add) in the definition 199 | subject.memoize(:increment) 200 | subject.memoize(:exception) 201 | end 202 | end 203 | end 204 | end 205 | -------------------------------------------------------------------------------- /lib/functional/union.rb: -------------------------------------------------------------------------------- 1 | require 'functional/abstract_struct' 2 | require 'functional/synchronization' 3 | 4 | module Functional 5 | 6 | # An immutable data structure with multiple fields, only one of which 7 | # can be set at any given time. A `Union` is a convenient way to bundle a 8 | # number of field attributes together, using accessor methods, without having 9 | # to write an explicit class. 10 | # 11 | # The `Union` module generates new `AbstractStruct` subclasses that hold a set of 12 | # fields with one and only one value associated with a single field. For each 13 | # field a reader method is created along with a predicate and a factory. The 14 | # predicate method indicates whether or not the give field is set. The reader 15 | # method returns the value of that field or `nil` when not set. The factory 16 | # creates a new union with the appropriate field set with the given value. 17 | # 18 | # A `Union` is very similar to a Ruby `Struct` and shares many of its behaviors 19 | # and attributes. Where a `Struct` can have zero or more values, each of which is 20 | # assiciated with a field, a `Union` can have one and only one value. Unlike a 21 | # Ruby `Struct`, a `Union` is immutable: its value is set at construction and 22 | # it can never be changed. Divergence between the two classes derive from these 23 | # two core differences. 24 | # 25 | # @example Creating a New Class 26 | # 27 | # LeftRightCenter = Functional::Union.new(:left, :right, :center) #=> LeftRightCenter 28 | # LeftRightCenter.ancestors #=> [LeftRightCenter, Functional::AbstractStruct... ] 29 | # LeftRightCenter.fields #=> [:left, :right, :center] 30 | # 31 | # prize = LeftRightCenter.right('One million dollars!') #=> # 32 | # prize.fields #=> [:left, :right, :center] 33 | # prize.values #=> [nil, "One million dollars!", nil] 34 | # 35 | # prize.left? #=> false 36 | # prize.right? #=> true 37 | # prize.center? #=> false 38 | # 39 | # prize.left #=> nil 40 | # prize.right #=> "One million dollars!" 41 | # prize.center #=> nil 42 | # 43 | # @example Registering a New Class with Union 44 | # 45 | # Functional::Union.new('Suit', :clubs, :diamonds, :hearts, :spades) 46 | # #=> Functional::Union::Suit 47 | # 48 | # Functional::Union::Suit.hearts('Queen') 49 | # #=> #nil, :diamonds=>nil, :hearts=>"Queen", :spades=>nil> 50 | # 51 | # @see Functional::Union 52 | # @see http://www.ruby-doc.org/core-2.1.2/Struct.html Ruby `Struct` class 53 | # @see http://en.wikipedia.org/wiki/Union_type "Union type" on Wikipedia 54 | # 55 | # @!macro thread_safe_immutable_object 56 | module Union 57 | extend self 58 | 59 | # Create a new union class with the given fields. 60 | # 61 | # @return [Functional::AbstractStruct] the new union subclass 62 | # @raise [ArgumentError] no fields specified 63 | def new(*fields) 64 | raise ArgumentError.new('no fields provided') if fields.empty? 65 | build(fields) 66 | end 67 | 68 | private 69 | 70 | # Use the given `AbstractStruct` class and build the methods necessary 71 | # to support the given data fields. 72 | # 73 | # @param [Array] fields the list of symbolic names for all data fields 74 | # @return [Functional::AbstractStruct] the union class 75 | def build(fields) 76 | union, fields = AbstractStruct.define_class(self, :union, fields) 77 | union.private_class_method(:new) 78 | define_properties(union) 79 | define_initializer(union) 80 | fields.each do |field| 81 | define_reader(union, field) 82 | define_predicate(union, field) 83 | define_factory(union, field) 84 | end 85 | union 86 | end 87 | 88 | # Define the `field` and `value` attribute readers on the given union class. 89 | # 90 | # @param [Functional::AbstractStruct] union the new union class 91 | # @return [Functional::AbstractStruct] the union class 92 | def define_properties(union) 93 | union.send(:attr_reader, :field) 94 | union.send(:attr_reader, :value) 95 | union 96 | end 97 | 98 | # Define a predicate method on the given union class for the given data field. 99 | # 100 | # @param [Functional::AbstractStruct] union the new union class 101 | # @param [Symbol] field symbolic name of the current data field 102 | # @return [Functional::AbstractStruct] the union class 103 | def define_predicate(union, field) 104 | union.send(:define_method, "#{field}?".to_sym) do 105 | @field == field 106 | end 107 | union 108 | end 109 | 110 | # Define a reader method on the given union class for the given data field. 111 | # 112 | # @param [Functional::AbstractStruct] union the new union class 113 | # @param [Symbol] field symbolic name of the current data field 114 | # @return [Functional::AbstractStruct] the union class 115 | def define_reader(union, field) 116 | union.send(:define_method, field) do 117 | send("#{field}?".to_sym) ? @value : nil 118 | end 119 | union 120 | end 121 | 122 | # Define an initializer method on the given union class. 123 | # 124 | # @param [Functional::AbstractStruct] union the new union class 125 | # @return [Functional::AbstractStruct] the union class 126 | def define_initializer(union) 127 | union.send(:define_method, :initialize) do |field, value| 128 | super() 129 | @field = field 130 | @value = value 131 | data = fields.reduce({}) do |memo, field| 132 | memo[field] = ( field == @field ? @value : nil ) 133 | memo 134 | end 135 | set_data_hash(data) 136 | set_values_array(data.values) 137 | ensure_ivar_visibility! 138 | self.freeze 139 | end 140 | union 141 | end 142 | 143 | # Define a factory method on the given union class for the given data field. 144 | # 145 | # @param [Functional::AbstractStruct] union the new union class 146 | # @param [Symbol] field symbolic name of the current data field 147 | # @return [Functional::AbstractStruct] the union class 148 | def define_factory(union, field) 149 | union.class.send(:define_method, field) do |value| 150 | new(field, value).freeze 151 | end 152 | union 153 | end 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /lib/functional/protocol.rb: -------------------------------------------------------------------------------- 1 | require 'functional/protocol_info' 2 | 3 | module Functional 4 | 5 | # An exception indicating a problem during protocol processing. 6 | ProtocolError = Class.new(StandardError) 7 | 8 | # Specify a new protocol or retrieve the specification of an existing 9 | # protocol. 10 | # 11 | # When called without a block the global protocol registry will be searched 12 | # for a protocol with the matching name. If found the corresponding 13 | # {Functional::ProtocolInfo} object will be returned. If not found `nil` will 14 | # be returned. 15 | # 16 | # When called with a block, a new protocol with the given name will be 17 | # created and the block will be processed to provide the specifiction. 18 | # When successful the new {Functional::ProtocolInfo} object will be returned. 19 | # An exception will be raised if a protocol with the same name already 20 | # exists. 21 | # 22 | # @example 23 | # Functional::SpecifyProtocol(:Queue) do 24 | # instance_method :push, 1 25 | # instance_method :pop, 0 26 | # instance_method :length, 0 27 | # end 28 | # 29 | # @param [Symbol] name The global name of the new protocol 30 | # @yield The protocol definition 31 | # @return [Functional::ProtocolInfo] the newly created or already existing 32 | # protocol specification 33 | # 34 | # @raise [Functional::ProtocolError] when attempting to specify a protocol 35 | # that has already been specified. 36 | # 37 | # @see Functional::Protocol 38 | def SpecifyProtocol(name, &block) 39 | name = name.to_sym 40 | protocol_info = Protocol.class_variable_get(:@@info)[name] 41 | 42 | return protocol_info unless block_given? 43 | 44 | if block_given? && protocol_info 45 | raise ProtocolError.new(":#{name} has already been defined") 46 | end 47 | 48 | info = ProtocolInfo.new(name, &block) 49 | Protocol.class_variable_get(:@@info)[name] = info 50 | end 51 | module_function :SpecifyProtocol 52 | 53 | # Protocols provide a polymorphism and method-dispatch mechanism that eschews 54 | # strong typing and embraces the dynamic duck typing of Ruby. Rather than 55 | # interrogate a module, class, or object for its type and ancestry, protocols 56 | # allow modules, classes, and methods to be interrogated based on their behavior. 57 | # It is a logical extension of the `respond_to?` method, but vastly more powerful. 58 | # 59 | # {include:file:doc/protocol.md} 60 | module Protocol 61 | 62 | # The global registry of specified protocols. 63 | @@info = {} 64 | 65 | # Does the given module/class/object fully satisfy the given protocol(s)? 66 | # 67 | # @param [Object] target the method/class/object to interrogate 68 | # @param [Symbol] protocols one or more protocols to check against the target 69 | # @return [Boolean] true if the target satisfies all given protocols else false 70 | # 71 | # @raise [ArgumentError] when no protocols given 72 | def Satisfy?(target, *protocols) 73 | raise ArgumentError.new('no protocols given') if protocols.empty? 74 | protocols.all?{|protocol| Protocol.satisfies?(target, protocol.to_sym) } 75 | end 76 | module_function :Satisfy? 77 | 78 | # Does the given module/class/object fully satisfy the given protocol(s)? 79 | # Raises a {Functional::ProtocolError} on failure. 80 | # 81 | # @param [Object] target the method/class/object to interrogate 82 | # @param [Symbol] protocols one or more protocols to check against the target 83 | # @return [Symbol] the target 84 | # 85 | # @raise [Functional::ProtocolError] when one or more protocols are not satisfied 86 | # @raise [ArgumentError] when no protocols given 87 | def Satisfy!(target, *protocols) 88 | Protocol::Satisfy?(target, *protocols) or 89 | Protocol.error(target, 'does not', *protocols) 90 | target 91 | end 92 | module_function :Satisfy! 93 | 94 | # Have the given protocols been specified? 95 | # 96 | # @param [Symbol] protocols the list of protocols to check 97 | # @return [Boolean] true if all given protocols have been specified else false 98 | # 99 | # @raise [ArgumentError] when no protocols are given 100 | def Specified?(*protocols) 101 | raise ArgumentError.new('no protocols given') if protocols.empty? 102 | Protocol.unspecified(*protocols).empty? 103 | end 104 | module_function :Specified? 105 | 106 | # Have the given protocols been specified? 107 | # Raises a {Functional::ProtocolError} on failure. 108 | # 109 | # @param [Symbol] protocols the list of protocols to check 110 | # @return [Boolean] true if all given protocols have been specified 111 | # 112 | # @raise [Functional::ProtocolError] if one or more of the given protocols have 113 | # not been specified 114 | # @raise [ArgumentError] when no protocols are given 115 | def Specified!(*protocols) 116 | raise ArgumentError.new('no protocols given') if protocols.empty? 117 | (unspecified = Protocol.unspecified(*protocols)).empty? or 118 | raise ProtocolError.new("The following protocols are unspecified: :#{unspecified.join('; :')}.") 119 | end 120 | module_function :Specified! 121 | 122 | private 123 | 124 | # Does the target satisfy the given protocol? 125 | # 126 | # @param [Object] target the module/class/object to check 127 | # @param [Symbol] protocol the protocol to check against the target 128 | # @return [Boolean] true if the target satisfies the protocol else false 129 | def self.satisfies?(target, protocol) 130 | info = @@info[protocol] 131 | return info && info.satisfies?(target) 132 | end 133 | 134 | # Reduces a list of protocols to a list of unspecified protocols. 135 | # 136 | # @param [Symbol] protocols the list of protocols to check 137 | # @return [Array] zero or more unspecified protocols 138 | def self.unspecified(*protocols) 139 | protocols.drop_while do |protocol| 140 | @@info.has_key? protocol.to_sym 141 | end 142 | end 143 | 144 | # Raise a {Functional::ProtocolError} formatted with the given data. 145 | # 146 | # @param [Object] target the object that was being interrogated 147 | # @param [String] message the message fragment to inject into the error 148 | # @param [Symbol] protocols list of protocols that were being checked against the target 149 | # 150 | # @raise [Functional::ProtocolError] the formatted exception object 151 | def self.error(target, message, *protocols) 152 | target = target.class unless target.is_a?(Module) 153 | raise ProtocolError, 154 | "Value (#{target.class}) '#{target}' #{message} behave as all of: :#{protocols.join('; :')}." 155 | end 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /lib/functional/pattern_matching.rb: -------------------------------------------------------------------------------- 1 | require 'functional/method_signature' 2 | 3 | module Functional 4 | 5 | # As much as I love Ruby I've always been a little disappointed that Ruby 6 | # doesn't support function overloading. Function overloading tends to reduce 7 | # branching and keep function signatures simpler. No sweat, I learned to do 8 | # without. Then I started programming in Erlang. My favorite Erlang feature 9 | # is, without question, pattern matching. Pattern matching is like function 10 | # overloading cranked to 11. So one day I was musing on Twitter that I'd like 11 | # to see Erlang-stype pattern matching in Ruby and one of my friends responded 12 | # "Build it!" So I did. And here it is. 13 | # 14 | # {include:file:doc/pattern_matching.md} 15 | module PatternMatching 16 | 17 | # A parameter that is required but that can take any value. 18 | # @!visibility private 19 | UNBOUND = Object.new.freeze 20 | 21 | # A match for one or more parameters in the last position of the match. 22 | # @!visibility private 23 | ALL = Object.new.freeze 24 | 25 | private 26 | 27 | # A guard clause on a pattern match. 28 | # @!visibility private 29 | GuardClause = Class.new do 30 | def initialize(function, clazz, pattern) 31 | @function = function 32 | @clazz = clazz 33 | @pattern = pattern 34 | end 35 | def when(&block) 36 | unless block_given? 37 | raise ArgumentError.new("block missing for `when` guard on function `#{@function}` of class #{@clazz}") 38 | end 39 | @pattern.guard = block 40 | self 41 | end 42 | end 43 | private_constant :GuardClause 44 | 45 | # @!visibility private 46 | FunctionPattern = Struct.new(:function, :args, :body, :guard) 47 | private_constant :FunctionPattern 48 | 49 | # @!visibility private 50 | def __unbound_args__(match, args) 51 | argv = [] 52 | match.args.each_with_index do |p, i| 53 | if p == ALL && i == match.args.length-1 54 | # when got ALL, then push all to the end to the list of args, 55 | # so we can get them as usual *args in matched method 56 | argv.concat args[(i..args.length)] 57 | elsif p.is_a?(Hash) && p.values.include?(UNBOUND) 58 | p.each do |key, value| 59 | argv << args[i][key] if value == UNBOUND 60 | end 61 | elsif p.is_a?(Hash) || p == UNBOUND || p.is_a?(Class) 62 | argv << args[i] 63 | end 64 | end 65 | argv 66 | end 67 | 68 | def __pass_guard__?(matcher, args) 69 | matcher.guard.nil? || 70 | self.instance_exec(*__unbound_args__(matcher, args), &matcher.guard) 71 | end 72 | 73 | # @!visibility private 74 | def __pattern_match__(clazz, function, *args, &block) 75 | args = args.first 76 | matchers = clazz.__function_pattern_matches__.fetch(function, []) 77 | matchers.detect do |matcher| 78 | MethodSignature.match?(matcher.args, args) && __pass_guard__?(matcher, args) 79 | end 80 | end 81 | 82 | # @!visibility private 83 | def self.included(base) 84 | base.extend(ClassMethods) 85 | super(base) 86 | end 87 | 88 | # Class methods added to a class that includes {Functional::PatternMatching} 89 | # @!visibility private 90 | module ClassMethods 91 | 92 | # @!visibility private 93 | def _() 94 | UNBOUND 95 | end 96 | 97 | # @!visibility private 98 | def defn(function, *args, &block) 99 | unless block_given? 100 | raise ArgumentError.new("block missing for definition of function `#{function}` on class #{self}") 101 | end 102 | 103 | # Check that number of free variables in pattern match method's arity 104 | pat_arity = __pattern_arity__(args) 105 | unless pat_arity == block.arity 106 | raise ArgumentError.new("Pattern and block arity mismatch: "\ 107 | "#{pat_arity}, #{block.arity}") 108 | end 109 | 110 | # add a new pattern for this function 111 | pattern = __register_pattern__(function, *args, &block) 112 | 113 | # define the delegator function if it doesn't exist yet 114 | unless self.instance_methods(false).include?(function) 115 | __define_method_with_matching__(function) 116 | end 117 | 118 | # return a guard clause to be added to the pattern 119 | GuardClause.new(function, self, pattern) 120 | end 121 | 122 | # @!visibility private 123 | # define an arity -1 function that dispatches to the appropriate 124 | # pattern match variant or raises an exception 125 | def __define_method_with_matching__(function) 126 | define_method(function) do |*args, &block| 127 | begin 128 | # get the collection of matched patterns for this function 129 | # use owner to ensure we climb the inheritance tree 130 | match = __pattern_match__(self.method(function).owner, function, args, block) 131 | if match 132 | # call the matched function 133 | argv = __unbound_args__(match, args) 134 | self.instance_exec(*argv, &match.body) 135 | elsif defined?(super) 136 | # delegate to the superclass 137 | super(*args, &block) 138 | else 139 | raise NoMethodError.new("no method `#{function}` matching "\ 140 | "#{args} found for class #{self.class}") 141 | end 142 | end 143 | end 144 | end 145 | 146 | # @!visibility private 147 | def __function_pattern_matches__ 148 | @__function_pattern_matches__ ||= Hash.new 149 | end 150 | 151 | # @!visibility private 152 | def __register_pattern__(function, *args, &block) 153 | block = Proc.new{} unless block_given? 154 | pattern = FunctionPattern.new(function, args, block) 155 | patterns = self.__function_pattern_matches__.fetch(function, []) 156 | patterns << pattern 157 | self.__function_pattern_matches__[function] = patterns 158 | pattern 159 | end 160 | 161 | # @!visibility private 162 | def __pattern_arity__(pat) 163 | r = pat.reduce(0) do |acc, v| 164 | if v.is_a?(Hash) 165 | ub = v.values.count { |e| e == UNBOUND } 166 | # if hash have UNBOUND then treat each unbound as separate arg 167 | # alse all hash is one arg 168 | ub > 0 ? acc + ub : acc + 1 169 | elsif v == ALL || v == UNBOUND || v.is_a?(Class) 170 | acc + 1 171 | else 172 | acc 173 | end 174 | end 175 | pat.last == ALL ? -r : r 176 | end 177 | 178 | end 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /spec/functional/complex_pattern_matching_spec.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | class Bar 4 | def greet 5 | return 'Hello, World!' 6 | end 7 | end 8 | 9 | class Foo < Bar 10 | include Functional::PatternMatching 11 | 12 | attr_accessor :name 13 | 14 | defn(:initialize) { @name = 'baz' } 15 | defn(:initialize, _) {|name| @name = name.to_s } 16 | 17 | defn(:greet, _) do |name| 18 | "Hello, #{name}!" 19 | end 20 | 21 | defn(:greet, :male, _) { |name| 22 | "Hello, Mr. #{name}!" 23 | } 24 | defn(:greet, :female, _) { |name| 25 | "Hello, Ms. #{name}!" 26 | } 27 | defn(:greet, nil, _) { |name| 28 | "Goodbye, #{name}!" 29 | } 30 | defn(:greet, _, _) { |_, name| 31 | "Hello, #{name}!" 32 | } 33 | 34 | defn(:hashable, _, {foo: :bar}, _) { |_, opts, _| 35 | :foo_bar 36 | } 37 | defn(:hashable, _, {foo: _, bar: _}, _) { |_, f, b, _| 38 | [f, b] 39 | } 40 | defn(:hashable, _, {foo: _}, _) { |_, f, _| 41 | f 42 | } 43 | defn(:hashable, _, {}, _) { |_,_,_| 44 | :empty 45 | } 46 | defn(:hashable, _, _, _) { |_, _, _| 47 | :unbound 48 | } 49 | 50 | defn(:options, _) { |opts| 51 | opts 52 | } 53 | 54 | defn(:recurse) { 55 | 'w00t!' 56 | } 57 | defn(:recurse, :match) { 58 | recurse() 59 | } 60 | defn(:recurse, :super) { 61 | greet() 62 | } 63 | defn(:recurse, :instance) { 64 | @name 65 | } 66 | defn(:recurse, _) { |arg| 67 | arg 68 | } 69 | 70 | defn(:concat, Integer, Integer) { |first, second| 71 | first + second 72 | } 73 | defn(:concat, Integer, String) { |first, second| 74 | "#{first} #{second}" 75 | } 76 | defn(:concat, String, String) { |first, second| 77 | first + second 78 | } 79 | defn(:concat, Integer, UNBOUND) { |first, second| 80 | first + second.to_i 81 | } 82 | 83 | defn(:all, :one, ALL) { |*args| 84 | args 85 | } 86 | defn(:all, :one, Integer, ALL) { |int, *args| 87 | [int, args] 88 | } 89 | defn(:all, 1, _, ALL) { |var, *args| 90 | [var, args] 91 | } 92 | defn(:all, ALL) { |*args| 93 | args 94 | } 95 | 96 | defn(:old_enough, _){ |_| true }.when{|x| x >= 16 } 97 | defn(:old_enough, _){ |_| false } 98 | 99 | defn(:right_age, _) { |_| 100 | true 101 | }.when{|x| x >= 16 && x <= 104 } 102 | 103 | defn(:right_age, _) { |_| 104 | false 105 | } 106 | 107 | defn(:wrong_age, _) { |_| 108 | true 109 | }.when{|x| x < 16 || x > 104 } 110 | 111 | defn(:wrong_age, _) { |_| 112 | false 113 | } 114 | end 115 | 116 | class Baz < Foo 117 | def boom_boom_room 118 | 'zoom zoom zoom' 119 | end 120 | def who(first, last) 121 | [first, last].join(' ') 122 | end 123 | end 124 | 125 | class Fizzbuzz < Baz 126 | include Functional::PatternMatching 127 | defn(:who, Integer) { |count| 128 | (1..count).each.reduce(:+) 129 | } 130 | defn(:who) { 0 } 131 | end 132 | 133 | describe 'complex pattern matching' do 134 | 135 | let(:name) { 'Pattern Matcher' } 136 | subject { Foo.new(name) } 137 | 138 | specify { expect(subject.greet).to eq 'Hello, World!' } 139 | 140 | specify { expect(subject.greet('Jerry')).to eq 'Hello, Jerry!' } 141 | 142 | specify { expect(subject.greet(:male, 'Jerry')).to eq 'Hello, Mr. Jerry!' } 143 | specify { expect(subject.greet(:female, 'Jeri')).to eq 'Hello, Ms. Jeri!' } 144 | specify { expect(subject.greet(:unknown, 'Jerry')).to eq 'Hello, Jerry!' } 145 | specify { expect(subject.greet(nil, 'Jerry')).to eq 'Goodbye, Jerry!' } 146 | 147 | # FIXME: This thing is failing because it can't match args that it got 148 | # and calling super, which can't handle it also and fail with ArgumentError 149 | # because super is usual ruby method, can't say what behavior here is 150 | # prefered (keep original ruby, or raise no method error somehow) 151 | # specify { 152 | # expect { Foo.new.greet(1,2,3,4,5,6,7) }.to raise_error(NoMethodError) 153 | # } 154 | 155 | specify { expect(subject.options(bar: :baz, one: 1, many: 2)).to eq({bar: :baz, one: 1, many: 2}) } 156 | 157 | specify { expect(subject.hashable(:male, {foo: :bar}, :female)).to eq :foo_bar } 158 | specify { expect(subject.hashable(:male, {foo: :baz}, :female)).to eq :baz } 159 | specify { expect(subject.hashable(:male, {foo: 1, bar: 2}, :female)).to eq [1, 2] } 160 | specify { expect(subject.hashable(:male, {foo: 1, baz: 2}, :female)).to eq 1 } 161 | specify { expect(subject.hashable(:male, {bar: :baz}, :female)).to eq :unbound } 162 | specify { expect(subject.hashable(:male, {}, :female)).to eq :empty } 163 | 164 | specify { expect(subject.recurse).to eq 'w00t!' } 165 | specify { expect(subject.recurse(:match)).to eq 'w00t!' } 166 | specify { expect(subject.recurse(:super)).to eq 'Hello, World!' } 167 | specify { expect(subject.recurse(:instance)).to eq name } 168 | specify { expect(subject.recurse(:foo)).to eq :foo } 169 | 170 | specify { expect(subject.concat(1, 1)).to eq 2 } 171 | specify { expect(subject.concat(1, 'shoe')).to eq '1 shoe' } 172 | specify { expect(subject.concat('shoe', 'fly')).to eq 'shoefly' } 173 | specify { expect(subject.concat(1, 2.9)).to eq 3 } 174 | 175 | specify { expect(subject.all(:one, 'a', 'bee', :see)).to eq(['a', 'bee', :see]) } 176 | specify { expect(subject.all(:one, 1, 'bee', :see)).to eq([1, 'bee', :see]) } 177 | specify { expect(subject.all(1, 'a', 'bee', :see)).to eq(['a', ['bee', :see]]) } 178 | specify { expect(subject.all('a', 'bee', :see)).to eq(['a', 'bee', :see]) } 179 | specify { expect { subject.all }.to raise_error(NoMethodError) } 180 | 181 | specify { expect(subject.old_enough(20)).to be true } 182 | specify { expect(subject.old_enough(10)).to be false } 183 | 184 | specify { expect(subject.right_age(20)).to be true } 185 | specify { expect(subject.right_age(10)).to be false } 186 | specify { expect(subject.right_age(110)).to be false } 187 | 188 | specify { expect(subject.wrong_age(20)).to be false } 189 | specify { expect(subject.wrong_age(10)).to be true } 190 | specify { expect(subject.wrong_age(110)).to be true } 191 | 192 | context 'inheritance' do 193 | 194 | specify { expect(Fizzbuzz.new.greet(:male, 'Jerry')).to eq 'Hello, Mr. Jerry!' } 195 | specify { expect(Fizzbuzz.new.greet(:female, 'Jeri')).to eq 'Hello, Ms. Jeri!' } 196 | specify { expect(Fizzbuzz.new.greet(:unknown, 'Jerry')).to eq 'Hello, Jerry!' } 197 | specify { expect(Fizzbuzz.new.greet(nil, 'Jerry')).to eq 'Goodbye, Jerry!' } 198 | 199 | specify { expect(Fizzbuzz.new.who(5)).to eq 15 } 200 | specify { expect(Fizzbuzz.new.who()).to eq 0 } 201 | # FIXME: same issue with Foo's super here 202 | # specify { 203 | # expect { 204 | # Fizzbuzz.new.who('Jerry', 'secret middle name', "D'Antonio") 205 | # }.to raise_error(NoMethodError) 206 | # } 207 | 208 | specify { expect(Fizzbuzz.new.boom_boom_room).to eq 'zoom zoom zoom' } 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /spec/functional/value_struct_spec.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | module Functional 4 | 5 | describe ValueStruct do 6 | 7 | context 'instanciation' do 8 | 9 | specify 'raises an exception when no arguments given' do 10 | expect { 11 | ValueStruct.new 12 | }.to raise_error(ArgumentError) 13 | end 14 | 15 | specify 'with a hash sets fields using has values' do 16 | subject = ValueStruct.new(foo: 1, 'bar' => :two, baz: 'three') 17 | expect(subject.foo).to eq 1 18 | expect(subject.bar).to eq :two 19 | expect(subject.baz).to eq 'three' 20 | end 21 | 22 | specify 'with a hash creates true predicates for has keys' do 23 | subject = ValueStruct.new(foo: 1, 'bar' => :two, baz: 'three') 24 | expect(subject.foo?).to be true 25 | expect(subject.bar?).to be true 26 | expect(subject.baz?).to be true 27 | end 28 | 29 | specify 'can be created from any object that responds to #each_pair' do 30 | clazz = Class.new do 31 | def each_pair(&block) 32 | {answer: 42, harmless: 'mostly'}.each_pair(&block) 33 | end 34 | end 35 | struct = clazz.new 36 | subject = ValueStruct.new(struct) 37 | expect(subject.answer).to eq 42 38 | expect(subject.harmless).to eq 'mostly' 39 | end 40 | 41 | specify 'raises an exception if given a non-hash argument' do 42 | expect { 43 | ValueStruct.new(:bogus) 44 | }.to raise_error(ArgumentError) 45 | end 46 | end 47 | 48 | context 'set fields' do 49 | 50 | subject { ValueStruct.new(foo: 42, bar: "Don't Panic") } 51 | 52 | specify 'have a reader which returns the value' do 53 | expect(subject.foo).to eq 42 54 | expect(subject.bar).to eq "Don't Panic" 55 | end 56 | 57 | specify 'have a predicate which returns true' do 58 | expect(subject.foo?).to be true 59 | expect(subject.bar?).to be true 60 | end 61 | end 62 | 63 | context 'unset fields' do 64 | 65 | subject { ValueStruct.new(foo: 42, bar: "Don't Panic") } 66 | 67 | specify 'have a magic predicate that always returns false' do 68 | expect(subject.baz?).to be false 69 | end 70 | end 71 | 72 | context 'accessors' do 73 | 74 | let!(:field_value_pairs) { {foo: 1, bar: :two, baz: 'three'} } 75 | 76 | subject { ValueStruct.new(field_value_pairs) } 77 | 78 | specify '#get returns the value of a set field' do 79 | expect(subject.get(:foo)).to eq 1 80 | end 81 | 82 | specify '#get returns nil for an unset field' do 83 | expect(subject.get(:bogus)).to be nil 84 | end 85 | 86 | specify '#[] is an alias for #get' do 87 | expect(subject[:foo]).to eq 1 88 | expect(subject[:bogus]).to be nil 89 | end 90 | 91 | specify '#set? returns false for an unset field' do 92 | expect(subject.set?(:harmless)).to be false 93 | end 94 | 95 | specify '#set? returns true for a field that has been set' do 96 | subject = ValueStruct.new(harmless: 'mostly') 97 | expect(subject.set?(:harmless)).to be true 98 | end 99 | 100 | specify '#fetch gets the value of a set field' do 101 | subject = ValueStruct.new(harmless: 'mostly') 102 | expect(subject.fetch(:harmless, 'extremely')).to eq 'mostly' 103 | end 104 | 105 | specify '#fetch returns the given value when the field is unset' do 106 | expect(subject.fetch(:harmless, 'extremely')).to eq 'extremely' 107 | end 108 | 109 | specify '#fetch does not set an unset field' do 110 | subject.fetch(:answer, 42) 111 | expect { 112 | subject.answer 113 | }.to raise_error(NoMethodError) 114 | end 115 | 116 | specify '#to_h returns the key/value pairs for all set values' do 117 | subject = ValueStruct.new(field_value_pairs) 118 | expect(subject.to_h).to eq field_value_pairs 119 | expect(subject.to_h).to_not be_frozen 120 | end 121 | 122 | specify '#each_pair returns an Enumerable when no block given' do 123 | subject = ValueStruct.new(field_value_pairs) 124 | expect(subject.each_pair).to be_a Enumerable 125 | end 126 | 127 | specify '#each_pair enumerates over each field/value pair' do 128 | subject = ValueStruct.new(field_value_pairs) 129 | result = {} 130 | 131 | subject.each_pair do |field, value| 132 | result[field] = value 133 | end 134 | 135 | expect(result).to eq field_value_pairs 136 | end 137 | end 138 | 139 | context 'reflection' do 140 | 141 | specify '#eql? returns true when both define the same fields with the same values' do 142 | first = ValueStruct.new(foo: 1, 'bar' => :two, baz: 'three') 143 | second = ValueStruct.new(foo: 1, 'bar' => :two, baz: 'three') 144 | 145 | expect(first.eql?(second)).to be true 146 | expect(first == second).to be true 147 | end 148 | 149 | specify '#eql? returns false when other has different fields defined' do 150 | first = ValueStruct.new(foo: 1, 'bar' => :two, baz: 'three') 151 | second = ValueStruct.new(foo: 1, 'bar' => :two) 152 | 153 | expect(first.eql?(second)).to be false 154 | expect(first == second).to be false 155 | end 156 | 157 | specify '#eql? returns false when other has different field values' do 158 | first = ValueStruct.new(foo: 1, 'bar' => :two, baz: 'three') 159 | second = ValueStruct.new(foo: 1, 'bar' => :two, baz: 3) 160 | 161 | expect(first.eql?(second)).to be false 162 | expect(first == second).to be false 163 | end 164 | 165 | specify '#eql? returns false when other is not a ValueStruct' do 166 | attributes = {answer: 42, harmless: 'mostly'} 167 | clazz = Class.new do 168 | def to_h; {answer: 42, harmless: 'mostly'}; end 169 | end 170 | 171 | other = clazz.new 172 | subject = ValueStruct.new(attributes) 173 | 174 | expect(subject.eql?(other)).to be false 175 | expect(subject == other).to be false 176 | end 177 | 178 | specify '#inspect begins with the class name' do 179 | subject = ValueStruct.new(foo: 1, 'bar' => :two, baz: 'three') 180 | expect(subject.inspect).to match(/^#<#{described_class}\s+/) 181 | end 182 | 183 | specify '#inspect includes all field/value pairs' do 184 | field_value_pairs = {foo: 1, 'bar' => :two, baz: 'three'} 185 | subject = ValueStruct.new(field_value_pairs) 186 | 187 | field_value_pairs.each do |field, value| 188 | expect(subject.inspect).to match(/:#{field}=>"?:?#{value}"?/) 189 | end 190 | end 191 | 192 | specify '#to_s returns the same value as #inspect' do 193 | subject = ValueStruct.new(foo: 1, 'bar' => :two, baz: 'three') 194 | expect(subject.to_s).to eq subject.inspect 195 | end 196 | end 197 | end 198 | end 199 | -------------------------------------------------------------------------------- /lib/functional/protocol_info.rb: -------------------------------------------------------------------------------- 1 | require 'functional/synchronization' 2 | 3 | module Functional 4 | 5 | # An immutable object describing a single protocol and capable of building 6 | # itself from a block. Used by {Functional#SpecifyProtocol}. 7 | # 8 | # @see Functional::Protocol 9 | class ProtocolInfo < Synchronization::Object 10 | 11 | # The symbolic name of the protocol 12 | attr_reader :name 13 | 14 | # Process a protocol specification block and build a new object. 15 | # 16 | # @param [Symbol] name the symbolic name of the protocol 17 | # @yield self to the given specification block 18 | # @return [Functional::ProtocolInfo] the new info object, frozen 19 | # 20 | # @raise [ArgumentError] when name is nil or an empty string 21 | # @raise [ArgumentError] when no block given 22 | def initialize(name, &specification) 23 | raise ArgumentError.new('no block given') unless block_given? 24 | raise ArgumentError.new('no name given') if name.nil? || name.empty? 25 | super 26 | @name = name.to_sym 27 | @info = Info.new({}, {}, []) 28 | self.instance_eval(&specification) 29 | @info.each_pair{|col, _| col.freeze} 30 | @info.freeze 31 | ensure_ivar_visibility! 32 | self.freeze 33 | end 34 | 35 | # The instance methods expected by this protocol. 36 | # 37 | # @return [Hash] a frozen hash of all instance method names and their 38 | # expected arity for this protocol 39 | def instance_methods 40 | @info.instance_methods 41 | end 42 | 43 | # The class methods expected by this protocol. 44 | # 45 | # @return [Hash] a frozen hash of all class method names and their 46 | # expected arity for this protocol 47 | def class_methods 48 | @info.class_methods 49 | end 50 | 51 | # The constants expected by this protocol. 52 | # 53 | # @return [Array] a frozen list of the constants expected by this protocol 54 | def constants 55 | @info.constants 56 | end 57 | 58 | # Does the given module/class/object satisfy this protocol? 59 | # 60 | # @return [Boolean] true if the target satisfies this protocol else false 61 | def satisfies?(target) 62 | satisfies_constants?(target) && 63 | satisfies_instance_methods?(target) && 64 | satisfies_class_methods?(target) 65 | end 66 | 67 | private 68 | 69 | # Data structure for encapsulating the protocol info data. 70 | # @!visibility private 71 | Info = Struct.new(:instance_methods, :class_methods, :constants) 72 | 73 | # Does the target satisfy the constants expected by this protocol? 74 | # 75 | # @param [target] target the module/class/object to interrogate 76 | # @return [Boolean] true when satisfied else false 77 | def satisfies_constants?(target) 78 | clazz = target.is_a?(Module) ? target : target.class 79 | @info.constants.all?{|constant| clazz.const_defined?(constant) } 80 | end 81 | 82 | # Does the target satisfy the instance methods expected by this protocol? 83 | # 84 | # @param [target] target the module/class/object to interrogate 85 | # @return [Boolean] true when satisfied else false 86 | def satisfies_instance_methods?(target) 87 | @info.instance_methods.all? do |method, arity| 88 | if target.is_a? Module 89 | target.method_defined?(method) && check_arity?(target.instance_method(method), arity) 90 | else 91 | target.respond_to?(method) && check_arity?(target.method(method), arity) 92 | end 93 | end 94 | end 95 | 96 | 97 | # Does the target satisfy the class methods expected by this protocol? 98 | # 99 | # @param [target] target the module/class/object to interrogate 100 | # @return [Boolean] true when satisfied else false 101 | def satisfies_class_methods?(target) 102 | clazz = target.is_a?(Module) ? target : target.class 103 | @info.class_methods.all? do |method, arity| 104 | break false unless clazz.respond_to? method 105 | method = clazz.method(method) 106 | check_arity?(method, arity) 107 | end 108 | end 109 | 110 | # Does the given method have the expected arity? Returns true 111 | # if the arity of the method is `-1` (variable length argument list 112 | # with no required arguments), when expected is `nil` (indicating any 113 | # arity is acceptable), or the arity of the method exactly matches the 114 | # expected arity. 115 | # 116 | # @param [Method] method the method object to interrogate 117 | # @param [Fixnum] expected the expected arity 118 | # @return [Boolean] true when an acceptable match else false 119 | # 120 | # @see http://www.ruby-doc.org/core-2.1.2/Method.html#method-i-arity Method#arity 121 | def check_arity?(method, expected) 122 | arity = method.arity 123 | expected.nil? || arity == -1 || expected == arity 124 | end 125 | 126 | ################################################################# 127 | # DSL methods 128 | 129 | # Specify an instance method. 130 | # 131 | # @param [Symbol] name the name of the method 132 | # @param [Fixnum] arity the required arity 133 | def instance_method(name, arity = nil) 134 | arity = arity.to_i unless arity.nil? 135 | @info.instance_methods[name.to_sym] = arity 136 | end 137 | 138 | # Specify a class method. 139 | # 140 | # @param [Symbol] name the name of the method 141 | # @param [Fixnum] arity the required arity 142 | def class_method(name, arity = nil) 143 | arity = arity.to_i unless arity.nil? 144 | @info.class_methods[name.to_sym] = arity 145 | end 146 | 147 | # Specify an instance reader attribute. 148 | # 149 | # @param [Symbol] name the name of the attribute 150 | def attr_reader(name) 151 | instance_method(name, 0) 152 | end 153 | 154 | # Specify an instance writer attribute. 155 | # 156 | # @param [Symbol] name the name of the attribute 157 | def attr_writer(name) 158 | instance_method("#{name}=".to_sym, 1) 159 | end 160 | 161 | # Specify an instance accessor attribute. 162 | # 163 | # @param [Symbol] name the name of the attribute 164 | def attr_accessor(name) 165 | attr_reader(name) 166 | attr_writer(name) 167 | end 168 | 169 | # Specify a class reader attribute. 170 | # 171 | # @param [Symbol] name the name of the attribute 172 | def class_attr_reader(name) 173 | class_method(name, 0) 174 | end 175 | 176 | # Specify a class writer attribute. 177 | # 178 | # @param [Symbol] name the name of the attribute 179 | def class_attr_writer(name) 180 | class_method("#{name}=".to_sym, 1) 181 | end 182 | 183 | # Specify a class accessor attribute. 184 | # 185 | # @param [Symbol] name the name of the attribute 186 | def class_attr_accessor(name) 187 | class_attr_reader(name) 188 | class_attr_writer(name) 189 | end 190 | 191 | # Specify a constant. 192 | # 193 | # @param [Symbol] name the name of the constant 194 | def constant(name) 195 | @info.constants << name.to_sym 196 | end 197 | end 198 | end 199 | -------------------------------------------------------------------------------- /doc/memo.md: -------------------------------------------------------------------------------- 1 | # memoize 2 | 3 | ### Rationale 4 | 5 | Many computational operations take a significant amount of time and/or use 6 | an inordinate amount of resources. If subsequent calls to that function with 7 | the same parameters are guaranteed to return the same result, caching the 8 | result can lead to significant performance improvements. The process of 9 | caching such calls is called 10 | [memoization](http://en.wikipedia.org/wiki/Memoization). 11 | 12 | ### Declaration 13 | 14 | Using memoization requires two simple steps: including the 15 | `Functional::Memo` module within a class or module and calling the `memoize` 16 | function to enable memoization on one or more methods. 17 | 18 | ```ruby 19 | Module EvenNumbers 20 | include Functional::Memoize 21 | 22 | self.first(n) 23 | (2..n).select{|i| i % 2 == 0 } 24 | end 25 | 26 | memoize :first 27 | end 28 | ``` 29 | 30 | When a function is memoized an internal cache is created that maps arguments 31 | to return values. When the function is called the arguments are checked 32 | against the cache. If the args are found the method is not called and the 33 | cached result is returned instead. 34 | 35 | ### Ramifications 36 | 37 | Memoizing long-running methods can lead to significant performance 38 | advantages. But there is a trade-off. Memoization may greatly increase the 39 | memory footprint of the application. The memo cache itself takes memory. The 40 | more arg/result pairs stored in the cache, the more memory is consumed. 41 | 42 | ##### Cache Size Options 43 | 44 | To help control the size of the cache, a limit can be placed on the number 45 | of items retained in the cache. The `:at_most` option, when given, indicates 46 | the maximum size of the cache. Once the maximum cache size is reached, calls 47 | to to the method with uncached args will still result in the method being 48 | called, but the results will not be cached. 49 | 50 | ```ruby 51 | Module EvenNumbers 52 | include Functional::Memoize 53 | 54 | self.first(n) 55 | (2..n).select{|i| i % 2 == 0 } 56 | end 57 | 58 | memoize :first, at_most: 1000 59 | end 60 | ``` 61 | 62 | There is no way to predict in advance what the proper cache size is, or if 63 | it should be restricted at all. Only performance testing under realistic 64 | conditions or profiling of a running system can provide guidance. 65 | 66 | ### Restrictions 67 | 68 | Not all methods are good candidates for memoization.Only methods that are 69 | [idempotent](http://en.wikipedia.org/wiki/Idempotence), [referentially 70 | transparent](http://en.wikipedia.org/wiki/Referential_transparency_(computer_science)), 71 | and free of [side effects](http://en.wikipedia.org/wiki/Side_effect_(computer_science)) 72 | can be effectively memoized. If a method creates side effects, such as 73 | writing to a log, only the first call to the method will create those side 74 | effects. Subsequent calls will return the cached value without calling the 75 | method. 76 | 77 | Similarly, methods which change internal state will only update the state on 78 | the initial call. Later calls will not result in state changes, they will 79 | only return the original result. Subsequently, instance methods cannot be 80 | memoized. Objects are, by definition, stateful. Method calls exist for the 81 | purpose of changing or using the internal state of the object. Such methods 82 | cannot be effectively memoized; it would require the internal state of the 83 | object to be cached and checked as well. 84 | 85 | Block parameters pose a similar problem. Block parameters are inherently 86 | stateful (they are closures which capture the enclosing context). And there 87 | is no way to check the state of the block along with the args to determine 88 | if the cached value should be used. Subsequently, and method call which 89 | includes a block will result in the cache being completely skipped. The base 90 | method will be called and the result will not be cached. This behavior will 91 | occur even when the given method was not programmed to accept a block 92 | parameter. Ruby will capture any block passed to any method and make it 93 | available to the method even when not documented as a formal parameter or 94 | used in the method. This has the interesting side effect of allowing the 95 | memo cache to be skipped on any method call, simply be passing a block 96 | parameter. 97 | 98 | ```ruby 99 | EvenNumbers.first(100) causes the result to be cached 100 | EvenNumbers.first(100) retrieves the previous result from the cache 101 | EvenNumbers.first(100){ nil } skips the memo cache and calls the method again 102 | ``` 103 | 104 | ### Complete Example 105 | 106 | The following example is borrowed from the book [Functional Thinking](http://shop.oreilly.com/product/0636920029687.do) 107 | by Neal Ford. In his book he shows an example of memoization in Groovy by 108 | summing factors of a given number. This is a great example because it 109 | exhibits all the criteria that make a method a good memoization candidate: 110 | 111 | * Idempotence 112 | * Referential transparency 113 | * Stateless 114 | * Free of side effect 115 | * Computationally expensive (for large numbers) 116 | 117 | The following code implements Ford's algorithms in Ruby, then memoizes two 118 | key methods. The Ruby code: 119 | 120 | ```ruby 121 | require 'functional' 122 | 123 | class Factors 124 | include Functional::Memo 125 | 126 | def self.sum_of(number) 127 | of(number).reduce(:+) 128 | end 129 | 130 | def self.of(number) 131 | (1..number).select {|i| factor?(number, i)} 132 | end 133 | 134 | def self.factor?(number, potential) 135 | number % potential == 0 136 | end 137 | 138 | def self.perfect?(number) 139 | sum_of(number) == 2 * number 140 | end 141 | 142 | def self.abundant?(number) 143 | sum_of(number) > 2 * number 144 | end 145 | 146 | def self.deficient?(number) 147 | sum_of(number) < 2 * number 148 | end 149 | 150 | memoize(:sum_of) 151 | memoize(:of) 152 | end 153 | ``` 154 | 155 | This code was tested in IRB using MRI 2.1.2 on a MacBook Pro. The `sum_of` 156 | method was called three times against the number 10,000,000 and the 157 | benchmark results of each run were captured. The test code: 158 | 159 | ```ruby 160 | require 'benchmark' 161 | 162 | 3.times do 163 | stats = Benchmark.measure do 164 | Factors.sum_of(10_000_000) 165 | end 166 | puts stats 167 | end 168 | ``` 169 | 170 | The results of the benchmarking are very revealing. The first run took over 171 | a second to calculate the results. The two subsequent runs, which retrieved 172 | the previous result from the memo cache, were nearly instantaneous: 173 | 174 | ``` 175 | 1.080000 0.000000 1.080000 ( 1.077524) 176 | 0.000000 0.000000 0.000000 ( 0.000033) 177 | 0.000000 0.000000 0.000000 ( 0.000008) 178 | ``` 179 | 180 | The same code run on the same computer using JRuby 1.7.12 exhibited similar 181 | results: 182 | 183 | ``` 184 | 1.800000 0.030000 1.830000 ( 1.494000) 185 | 0.000000 0.000000 0.000000 ( 0.000000) 186 | 0.000000 0.000000 0.000000 ( 0.000000) 187 | ``` 188 | 189 | ### Inspiration 190 | 191 | * [Memoization](http://en.wikipedia.org/wiki/Memoization) at Wikipedia 192 | * Clojure [memoize](http://clojuredocs.org/clojure_core/clojure.core/memoize) function 193 | -------------------------------------------------------------------------------- /lib/functional/option.rb: -------------------------------------------------------------------------------- 1 | require 'functional/abstract_struct' 2 | require 'functional/either' 3 | require 'functional/protocol' 4 | require 'functional/synchronization' 5 | 6 | Functional::SpecifyProtocol(:Option) do 7 | instance_method :some?, 0 8 | instance_method :none?, 0 9 | instance_method :some, 0 10 | end 11 | 12 | module Functional 13 | 14 | # An optional value that may be none (no value) or some (a value). 15 | # This type is a replacement for the use of nil with better type checks. 16 | # It is an immutable data structure that extends `AbstractStruct`. 17 | # 18 | # @see http://functionaljava.googlecode.com/svn/artifacts/3.0/javadoc/index.html Functional Java 19 | # 20 | # @!macro thread_safe_immutable_object 21 | class Option < Synchronization::Object 22 | include AbstractStruct 23 | 24 | # @!visibility private 25 | NO_OPTION = Object.new.freeze 26 | 27 | self.datatype = :option 28 | self.fields = [:some].freeze 29 | 30 | private_class_method :new 31 | 32 | # The reason for the absence of a value when none, 33 | # defaults to nil 34 | attr_reader :reason 35 | 36 | class << self 37 | 38 | # Construct an `Option` with no value. 39 | # 40 | # @return [Option] the new option 41 | def none(reason = nil) 42 | new(nil, true, reason).freeze 43 | end 44 | 45 | # Construct an `Option` with the given value. 46 | # 47 | # @param [Object] value the value of the option 48 | # @return [Option] the new option 49 | def some(value) 50 | new(value, false).freeze 51 | end 52 | end 53 | 54 | # Does the option have a value? 55 | # 56 | # @return [Boolean] true if some else false 57 | def some? 58 | ! none? 59 | end 60 | alias_method :value?, :some? 61 | alias_method :fulfilled?, :some? 62 | 63 | # Is the option absent a value? 64 | # 65 | # @return [Boolean] true if none else false 66 | def none? 67 | @none 68 | end 69 | alias_method :reason?, :none? 70 | alias_method :rejected?, :none? 71 | 72 | # The value of this option. 73 | # 74 | # @return [Object] the value when some else nil 75 | def some 76 | to_h[:some] 77 | end 78 | alias_method :value, :some 79 | 80 | # Returns the length of this optional value; 81 | # 1 if there is a value, 0 otherwise. 82 | # 83 | # @return [Fixnum] The length of this optional value; 84 | # 1 if there is a value, 0 otherwise. 85 | def length 86 | none? ? 0 : 1 87 | end 88 | alias_method :size, :length 89 | 90 | # Perform a logical `and` operation against this option and the 91 | # provided option or block. Returns true if this option is some and: 92 | # 93 | # * other is an `Option` with some value 94 | # * other is a truthy value (not nil or false) 95 | # * the result of the block is a truthy value 96 | # 97 | # If a block is given the value of the current option is passed to the 98 | # block and the result of block processing will be evaluated for its 99 | # truthiness. An exception will be raised if an other value and a 100 | # block are both provided. 101 | # 102 | # @param [Object] other the value to be evaluated against this option 103 | # @yieldparam [Object] value the value of this option when some 104 | # @return [Boolean] true when the union succeeds else false 105 | # @raise [ArgumentError] when given both other and a block 106 | def and(other = NO_OPTION) 107 | raise ArgumentError.new('cannot give both an option and a block') if other != NO_OPTION && block_given? 108 | return false if none? 109 | 110 | if block_given? 111 | !! yield(some) 112 | elsif Protocol::Satisfy? other, :Option 113 | other.some? 114 | else 115 | !! other 116 | end 117 | end 118 | 119 | # Perform a logical `or` operation against this option and the 120 | # provided option or block. Returns true if this option is some. 121 | # If this option is none it returns true if: 122 | # 123 | # * other is an `Option` with some value 124 | # * other is a truthy value (not nil or false) 125 | # * the result of the block is a truthy value 126 | # 127 | # If a block is given the value of the result of block processing 128 | # will be evaluated for its truthiness. An exception will be raised 129 | # if an other value and a block are both provided. 130 | # 131 | # @param [Object] other the value to be evaluated against this option 132 | # @return [Boolean] true when the intersection succeeds else false 133 | # @raise [ArgumentError] when given both other and a block 134 | def or(other = NO_OPTION) 135 | raise ArgumentError.new('cannot give both an option and a block') if other != NO_OPTION && block_given? 136 | return true if some? 137 | 138 | if block_given? 139 | !! yield 140 | elsif Protocol::Satisfy? other, :Option 141 | other.some? 142 | else 143 | !! other 144 | end 145 | end 146 | 147 | # Returns the value of this option when some else returns the 148 | # value of the other option or block. When the other is also an 149 | # option its some value is returned. When the other is any other 150 | # value it is simply passed through. When a block is provided the 151 | # block is processed and the return value of the block is returned. 152 | # An exception will be raised if an other value and a block are 153 | # both provided. 154 | # 155 | # @param [Object] other the value to be evaluated when this is none 156 | # @return [Object] this value when some else the value of other 157 | # @raise [ArgumentError] when given both other and a block 158 | def else(other = NO_OPTION) 159 | raise ArgumentError.new('cannot give both an option and a block') if other != NO_OPTION && block_given? 160 | return some if some? 161 | 162 | if block_given? 163 | yield 164 | elsif Protocol::Satisfy? other, :Option 165 | other.some 166 | else 167 | other 168 | end 169 | end 170 | 171 | # If the condition satisfies, return the given A in some, otherwise, none. 172 | # 173 | # @param [Object] value The some value to use if the condition satisfies. 174 | # @param [Boolean] condition The condition to test (when no block given). 175 | # @yield The condition to test (when no condition given). 176 | # 177 | # @return [Option] A constructed option based on the given condition. 178 | # 179 | # @raise [ArgumentError] When both a condition and a block are given. 180 | def self.iff(value, condition = NO_OPTION) 181 | raise ArgumentError.new('requires either a condition or a block, not both') if condition != NO_OPTION && block_given? 182 | condition = block_given? ? yield : !! condition 183 | condition ? some(value) : none 184 | end 185 | 186 | # @!macro inspect_method 187 | def inspect 188 | super.gsub(/ :some/, " (#{some? ? 'some' : 'none'}) :some") 189 | end 190 | alias_method :to_s, :inspect 191 | 192 | private 193 | 194 | # Create a new Option with the given value and disposition. 195 | # 196 | # @param [Object] value the value of this option 197 | # @param [Boolean] none is this option absent a value? 198 | # @param [Object] reason the reason for the absense of a value 199 | # 200 | # @!visibility private 201 | def initialize(value, none, reason = nil) 202 | super 203 | @none = none 204 | @reason = none ? reason : nil 205 | hsh = none ? {some: nil} : {some: value} 206 | set_data_hash(hsh) 207 | set_values_array(hsh.values) 208 | ensure_ivar_visibility! 209 | end 210 | end 211 | end 212 | -------------------------------------------------------------------------------- /spec/functional/either_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'abstract_struct_shared' 2 | 3 | module Functional 4 | 5 | describe Either do 6 | 7 | let!(:value){ 42 } 8 | let!(:reason){ StandardError.new } 9 | 10 | let!(:expected_fields){ [:left, :right] } 11 | let!(:expected_values){ [value, nil] } 12 | 13 | let(:struct_class) { Either } 14 | let(:struct_object) { Either.left(value) } 15 | let(:other_object) { Either.left(Object.new) } 16 | 17 | let(:left_subject){ Either.left(reason) } 18 | let(:right_subject){ Either.right(value) } 19 | 20 | it_should_behave_like :abstract_struct 21 | 22 | specify{ Functional::Protocol::Satisfy! Either, :Either } 23 | specify{ Functional::Protocol::Satisfy! Either, :Disposition } 24 | 25 | context 'initialization' do 26 | 27 | it 'cannot be constructed directly' do 28 | expect { 29 | Either.new 30 | }.to raise_error(NameError) 31 | end 32 | 33 | it 'sets the left value when constructed by #left' do 34 | expect(Either.left(value).left).to eq value 35 | end 36 | 37 | it 'sets the right value when constructed by #right' do 38 | expect(Either.right(value).right).to eq value 39 | end 40 | 41 | it 'freezes the new object' do 42 | expect(Either.left(:foo)).to be_frozen 43 | expect(Either.right(:foo)).to be_frozen 44 | end 45 | 46 | it 'aliases #left to #reason' do 47 | expect(Either.reason(value).left).to eq value 48 | end 49 | 50 | it 'aliases #right to #value' do 51 | expect(Either.value(value).right).to eq value 52 | end 53 | 54 | context '#error' do 55 | 56 | it 'sets left to a StandardError with backtrace when no arguments given' do 57 | either = Either.error 58 | expect(either.left).to be_a StandardError 59 | expect(either.left.message).to_not be nil 60 | expect(either.left.backtrace).to_not be_empty 61 | end 62 | 63 | it 'sets left to a StandardError with the given message' do 64 | message = 'custom error message' 65 | either = Either.error(message) 66 | expect(either.left).to be_a StandardError 67 | expect(either.left.message).to eq message 68 | expect(either.left.backtrace).to_not be_empty 69 | end 70 | 71 | it 'sets left to an object of the given class with the given message' do 72 | message = 'custom error message' 73 | error_class = ArgumentError 74 | either = Either.error(message, error_class) 75 | expect(either.left).to be_a error_class 76 | expect(either.left.message).to eq message 77 | expect(either.left.backtrace).to_not be_empty 78 | end 79 | end 80 | end 81 | 82 | context 'state' do 83 | 84 | specify '#left? returns true when the left value is set' do 85 | expect(left_subject).to be_left 86 | end 87 | 88 | specify '#left? returns false when the right value is set' do 89 | expect(right_subject).to_not be_left 90 | end 91 | 92 | specify '#right? returns true when the right value is set' do 93 | expect(right_subject).to be_right 94 | end 95 | 96 | specify '#right? returns false when the left value is set' do 97 | expect(left_subject).to_not be_right 98 | end 99 | 100 | specify '#left returns the left value when left is set' do 101 | expect(left_subject.left).to eq reason 102 | end 103 | 104 | specify '#left returns nil when right is set' do 105 | expect(right_subject.left).to be_nil 106 | end 107 | 108 | specify '#right returns the right value when right is set' do 109 | expect(right_subject.right).to eq value 110 | end 111 | 112 | specify '#right returns nil when left is set' do 113 | expect(left_subject.right).to be_nil 114 | end 115 | 116 | specify 'aliases #left? as #reason?' do 117 | expect(left_subject.reason?).to be true 118 | end 119 | 120 | specify 'aliases #right? as #value?' do 121 | expect(right_subject.value?).to be true 122 | end 123 | 124 | specify 'aliases #left as #reason' do 125 | expect(left_subject.reason).to eq reason 126 | expect(right_subject.reason).to be_nil 127 | end 128 | 129 | specify 'aliases #right as #value' do 130 | expect(right_subject.value).to eq value 131 | expect(left_subject.value).to be_nil 132 | end 133 | end 134 | 135 | context '#swap' do 136 | 137 | it 'converts a left projection into a right projection' do 138 | subject = Either.left(:foo) 139 | swapped = subject.swap 140 | expect(swapped).to be_right 141 | expect(swapped.left).to be_nil 142 | expect(swapped.right).to eq :foo 143 | end 144 | 145 | it 'converts a right projection into a left projection' do 146 | subject = Either.right(:foo) 147 | swapped = subject.swap 148 | expect(swapped).to be_left 149 | expect(swapped.right).to be_nil 150 | expect(swapped.left).to eq :foo 151 | end 152 | end 153 | 154 | context '#either' do 155 | 156 | it 'passes the left value to the left proc when left' do 157 | expected = nil 158 | subject = Either.left(100) 159 | subject.either( 160 | ->(left) { expected = left }, 161 | ->(right) { expected = -1 } 162 | ) 163 | expect(expected).to eq 100 164 | end 165 | 166 | it 'returns the value of the left proc when left' do 167 | subject = Either.left(100) 168 | expect( 169 | subject.either( 170 | ->(left) { left * 2 }, 171 | ->(right) { nil } 172 | ) 173 | ).to eq 200 174 | end 175 | 176 | it 'passes the right value to the right proc when right' do 177 | expected = nil 178 | subject = Either.right(100) 179 | subject.either( 180 | ->(right) { expected = -1 }, 181 | ->(right) { expected = right } 182 | ) 183 | expect(expected).to eq 100 184 | end 185 | 186 | it 'returns the value of the right proc when right' do 187 | subject = Either.right(100) 188 | expect( 189 | subject.either( 190 | ->(right) { nil }, 191 | ->(right) { right * 2 } 192 | ) 193 | ).to eq 200 194 | end 195 | end 196 | 197 | context '#iff' do 198 | 199 | it 'returns a lefty with the given left value when the boolean is true' do 200 | subject = Either.iff(:foo, :bar, true) 201 | expect(subject).to be_left 202 | expect(subject.left).to eq :foo 203 | end 204 | 205 | it 'returns a righty with the given right value when the boolean is false' do 206 | subject = Either.iff(:foo, :bar, false) 207 | expect(subject).to be_right 208 | expect(subject.right).to eq :bar 209 | end 210 | 211 | it 'returns a lefty with the given left value when the block is truthy' do 212 | subject = Either.iff(:foo, :bar){ :baz } 213 | expect(subject).to be_left 214 | expect(subject.left).to eq :foo 215 | end 216 | 217 | it 'returns a righty with the given right value when the block is false' do 218 | subject = Either.iff(:foo, :bar){ false } 219 | expect(subject).to be_right 220 | expect(subject.right).to eq :bar 221 | end 222 | 223 | it 'returns a righty with the given right value when the block is nil' do 224 | subject = Either.iff(:foo, :bar){ nil } 225 | expect(subject).to be_right 226 | expect(subject.right).to eq :bar 227 | end 228 | 229 | it 'raises an exception when both a boolean and a block are given' do 230 | expect { 231 | subject = Either.iff(:foo, :bar, true){ nil } 232 | }.to raise_error(ArgumentError) 233 | end 234 | end 235 | end 236 | end 237 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Functional Ruby 2 | [![Gem Version](https://badge.fury.io/rb/functional-ruby.svg)](http://badge.fury.io/rb/functional-ruby) 3 | [![Travis CI Build Status](https://secure.travis-ci.org/jdantonio/functional-ruby.png)](https://travis-ci.org/jdantonio/functional-ruby?branch=master) 4 | [![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) 5 | [![Coverage Status](https://coveralls.io/repos/jdantonio/functional-ruby/badge.png)](https://coveralls.io/r/jdantonio/functional-ruby) 6 | [![Code Climate](https://codeclimate.com/github/jdantonio/functional-ruby.png)](https://codeclimate.com/github/jdantonio/functional-ruby) 7 | [![Inline docs](http://inch-ci.org/github/jdantonio/functional-ruby.png)](http://inch-ci.org/github/jdantonio/functional-ruby) 8 | [![Dependency Status](https://gemnasium.com/jdantonio/functional-ruby.png)](https://gemnasium.com/jdantonio/functional-ruby) 9 | [![License](http://img.shields.io/license/MIT.png?color=green)](http://opensource.org/licenses/MIT) 10 | 11 | **A gem for adding functional programming tools to Ruby. Inspired by [Erlang](http://www.erlang.org/), 12 | [Clojure](http://clojure.org/), and [Functional Java](http://functionaljava.org/).** 13 | 14 | ## Introduction 15 | 16 | Two things I love are [Ruby](http://www.ruby-lang.org/en/) and 17 | [functional](https://en.wikipedia.org/wiki/Functional_programming) 18 | [programming](http://c2.com/cgi/wiki?FunctionalProgramming). 19 | If you combine Ruby's ability to create functions sans-classes with the power 20 | of blocks, `proc`, and `lambda`, Ruby code can follow just about every modern functional 21 | programming design paradigm. Add to this Ruby's vast metaprogramming capabilities 22 | and Ruby is easily one of the most powerful languages in common use today. 23 | 24 | ### Goals 25 | 26 | Our goal is to implement various functional programming patterns in Ruby. Specifically: 27 | 28 | * Be an 'unopinionated' toolbox that provides useful utilities without debating which is better or why 29 | * Remain free of external gem dependencies 30 | * Stay true to the spirit of the languages providing inspiration 31 | * But implement in a way that makes sense for Ruby 32 | * Keep the semantics as idiomatic Ruby as possible 33 | * Support features that make sense in Ruby 34 | * Exclude features that don't make sense in Ruby 35 | * Keep everything small 36 | * Be as fast as reasonably possible 37 | 38 | ## Features 39 | 40 | The primary site for documentation is the automatically generated [API documentation](http://jdantonio.github.io/functional-ruby/). 41 | 42 | * Protocol specifications inspired by Clojure [protocol](http://clojure.org/protocols), 43 | Erlang [behavior](http://www.erlang.org/doc/design_principles/des_princ.html#id60128), 44 | and Objective-C [protocol](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithProtocols/WorkingwithProtocols.html). 45 | * Function overloading with Erlang-style [function](http://erlang.org/doc/reference_manual/functions.html) 46 | [pattern matching](http://erlang.org/doc/reference_manual/patterns.html). 47 | * Simple, thread safe, immutable data structures, such as `Record`, `Union`, and `Tuple`, inspired by 48 | [Clojure](http://clojure.org/datatypes), [Erlang](http://www.erlang.org/doc/reference_manual/records.html), 49 | and other functional languages. 50 | * 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). 51 | * [Memoization](http://en.wikipedia.org/wiki/Memoization) of class methods based on Clojure [memoize](http://clojuredocs.org/clojure_core/clojure.core/memoize). 52 | * Lazy execution with a `Delay` class based on Clojure [delay](http://clojuredocs.org/clojure_core/clojure.core/delay). 53 | * `ValueStruct`, a simple, thread safe, immutable variation of Ruby's [OpenStruct](http://ruby-doc.org/stdlib-2.0/libdoc/ostruct/rdoc/OpenStruct.html) class. 54 | * Thread safe data structures, such as `FinalStruct` and `FinalVar`, which can be written to at most once 55 | before becoming immutable. Based on [Java's `final` keyword](http://en.wikipedia.org/wiki/Final_(Java)). 56 | 57 | ### Supported Ruby Versions 58 | 59 | MRI 2.0 and higher, JRuby (1.9 mode), and Rubinius 2.x. This library is pure Ruby and has no gem dependencies. 60 | It should be fully compatible with any interpreter that is compliant with Ruby 2.0 or newer. 61 | 62 | ### Install 63 | 64 | ```shell 65 | gem install functional-ruby 66 | ``` 67 | 68 | or add the following line to Gemfile: 69 | 70 | ```ruby 71 | gem 'functional-ruby' 72 | ``` 73 | 74 | and run `bundle install` from your shell. 75 | 76 | Once you've installed the gem you must `require` it in your project: 77 | 78 | ```ruby 79 | require 'functional' 80 | ``` 81 | 82 | ## Examples 83 | 84 | Specifying a [protocol](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/Protocol): 85 | 86 | ```ruby 87 | Functional::SpecifyProtocol(:Name) do 88 | attr_accessor :first 89 | attr_accessor :middle 90 | attr_accessor :last 91 | attr_accessor :suffix 92 | end 93 | ``` 94 | 95 | Defining immutable [data structures](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/AbstractStruct) including 96 | [Either](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/Either), 97 | [Option](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/Option), 98 | [Union](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/Union) and 99 | [Record](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/Record) 100 | 101 | ```ruby 102 | Name = Functional::Record.new(:first, :middle, :last, :suffix) do 103 | mandatory :first, :last 104 | default :first, 'J.' 105 | default :last, 'Doe' 106 | end 107 | 108 | anon = Name.new #=> #"J.", :middle=>nil, :last=>"Doe", :suffix=>nil> 109 | matz = Name.new(first: 'Yukihiro', last: 'Matsumoto') #=> #"Yukihiro", :middle=>nil, :last=>"Matsumoto", :suffix=>nil> 110 | ``` 111 | 112 | [Pattern matching](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/PatternMatching) 113 | using [protocols](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/Protocol), 114 | [type](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/TypeCheck) checking, 115 | and other options: 116 | 117 | ```ruby 118 | class Foo 119 | include Functional::PatternMatching 120 | include Functional::Protocol 121 | include Functional::TypeCheck 122 | 123 | def greet 124 | return 'Hello, World!' 125 | end 126 | 127 | defn(:greet, _) do |name| 128 | "Hello, #{name}!" 129 | end 130 | 131 | defn(:greet, _) { |name| 132 | "Pleased to meet you, #{name.full_name}!" 133 | }.when {|name| Type?(name, CustomerModel, ClientModel) } 134 | 135 | defn(:greet, _) { |name| 136 | "Hello, #{name.first} #{name.last}!" 137 | }.when {|name| Satisfy?(name, :Name) } 138 | 139 | defn(:greet, :doctor, _) { |name| 140 | "Hello, Dr. #{name}!" 141 | } 142 | 143 | defn(:greet, nil, _) { |name| 144 | "Goodbye, #{name}!" 145 | } 146 | 147 | defn(:greet, _, _) { |_, name| 148 | "Hello, #{name}!" 149 | } 150 | end 151 | ``` 152 | 153 | Performance improvement of idempotent functions through [memoization](http://rubydoc.info/github/jdantonio/functional-ruby/master/Functional/Memo): 154 | 155 | ```ruby 156 | class Factors 157 | include Functional::Memo 158 | 159 | def self.sum_of(number) 160 | of(number).reduce(:+) 161 | end 162 | 163 | def self.of(number) 164 | (1..number).select {|i| factor?(number, i)} 165 | end 166 | 167 | def self.factor?(number, potential) 168 | number % potential == 0 169 | end 170 | 171 | memoize(:sum_of) 172 | memoize(:of) 173 | end 174 | ``` 175 | 176 | ## Contributing 177 | 178 | 1. Fork it 179 | 2. Create your feature branch (`git checkout -b my-new-feature`) 180 | 3. Commit your changes (`git commit -am 'Add some feature'`) 181 | 4. Push to the branch (`git push origin my-new-feature`) 182 | 5. Create new Pull Request 183 | 184 | ## License and Copyright 185 | 186 | *Functional Ruby* is free software released under the [MIT License](http://www.opensource.org/licenses/MIT). 187 | -------------------------------------------------------------------------------- /lib/functional/record.rb: -------------------------------------------------------------------------------- 1 | require 'functional/abstract_struct' 2 | require 'functional/protocol' 3 | require 'functional/type_check' 4 | 5 | module Functional 6 | 7 | # An immutable data structure with multiple data fields. A `Record` is a 8 | # convenient way to bundle a number of field attributes together, 9 | # using accessor methods, without having to write an explicit class. 10 | # The `Record` module generates new `AbstractStruct` subclasses that hold a 11 | # set of fields with a reader method for each field. 12 | # 13 | # A `Record` is very similar to a Ruby `Struct` and shares many of its behaviors 14 | # and attributes. Unlike a # Ruby `Struct`, a `Record` is immutable: its values 15 | # are set at construction and can never be changed. Divergence between the two 16 | # classes derive from this core difference. 17 | # 18 | # {include:file:doc/record.md} 19 | # 20 | # @see Functional::Union 21 | # @see Functional::Protocol 22 | # @see Functional::TypeCheck 23 | # 24 | # @!macro thread_safe_immutable_object 25 | module Record 26 | extend self 27 | 28 | # Create a new record class with the given fields. 29 | # 30 | # @return [Functional::AbstractStruct] the new record subclass 31 | # @raise [ArgumentError] no fields specified or an invalid type 32 | # specification is given 33 | def new(*fields, &block) 34 | raise ArgumentError.new('no fields provided') if fields.empty? 35 | 36 | name = nil 37 | types = nil 38 | 39 | # check if a name for registration is given 40 | if fields.first.is_a?(String) 41 | name = fields.first 42 | fields = fields[1..fields.length-1] 43 | end 44 | 45 | # check for a set of type/protocol specifications 46 | if fields.size == 1 && fields.first.respond_to?(:to_h) 47 | types = fields.first 48 | fields = fields.first.keys 49 | check_types!(types) 50 | end 51 | 52 | build(name, fields, types, &block) 53 | rescue 54 | raise ArgumentError.new('invalid specification') 55 | end 56 | 57 | private 58 | 59 | # @!visibility private 60 | # 61 | # A set of restrictions governing the creation of a new record. 62 | class Restrictions 63 | include Protocol 64 | include TypeCheck 65 | 66 | # Create a new restrictions object by processing the given 67 | # block. The block should be the DSL for defining a record class. 68 | # 69 | # @param [Hash] types a hash of fields and the associated type/protocol 70 | # when type/protocol checking is among the restrictions 71 | # @param [Proc] block A DSL definition of a new record. 72 | # @yield A DSL definition of a new record. 73 | def initialize(types = nil, &block) 74 | @types = types 75 | @required = [] 76 | @defaults = {} 77 | instance_eval(&block) if block_given? 78 | @required.freeze 79 | @defaults.freeze 80 | self.freeze 81 | end 82 | 83 | # DSL method for declaring one or more fields to be mandatory. 84 | # 85 | # @param [Symbol] fields zero or more mandatory fields 86 | def mandatory(*fields) 87 | @required.concat(fields.collect{|field| field.to_sym}) 88 | end 89 | 90 | # DSL method for declaring a default value for a field 91 | # 92 | # @param [Symbol] field the field to be given a default value 93 | # @param [Object] value the default value of the field 94 | def default(field, value) 95 | @defaults[field] = value 96 | end 97 | 98 | # Clone a default value if it is cloneable. Else just return 99 | # the value. 100 | # 101 | # @param [Symbol] field the name of the field from which the 102 | # default value is to be cloned. 103 | # @return [Object] a clone of the value or the value if uncloneable 104 | def clone_default(field) 105 | value = @defaults[field] 106 | value = value.clone unless uncloneable?(value) 107 | rescue TypeError 108 | # can't be cloned 109 | ensure 110 | return value 111 | end 112 | 113 | # Validate the record data against this set of restrictions. 114 | # 115 | # @param [Hash] data the data hash 116 | # @raise [ArgumentError] when the data does not match the restrictions 117 | def validate!(data) 118 | validate_mandatory!(data) 119 | validate_types!(data) 120 | end 121 | 122 | private 123 | 124 | # Check the given data hash to see if it contains non-nil values for 125 | # all mandatory fields. 126 | # 127 | # @param [Hash] data the data hash 128 | # @raise [ArgumentError] if any mandatory fields are missing 129 | def validate_mandatory!(data) 130 | if data.any?{|k,v| @required.include?(k) && v.nil? } 131 | raise ArgumentError.new('mandatory fields must not be nil') 132 | end 133 | end 134 | 135 | # Validate the record data against a type/protocol specification. 136 | # 137 | # @param [Hash] data the data hash 138 | # @raise [ArgumentError] when the data does not match the specification 139 | def validate_types!(data) 140 | return if @types.nil? 141 | @types.each do |field, type| 142 | value = data[field] 143 | next if value.nil? 144 | if type.is_a? Module 145 | raise ArgumentError.new("'#{field}' must be of type #{type}") unless Type?(value, type) 146 | else 147 | raise ArgumentError.new("'#{field}' must stasify the protocol :#{type}") unless Satisfy?(value, type) 148 | end 149 | end 150 | end 151 | 152 | # Is the given object uncloneable? 153 | # 154 | # @param [Object] object the object to check 155 | # @return [Boolean] true if the object cannot be cloned else false 156 | def uncloneable?(object) 157 | Type? object, NilClass, TrueClass, FalseClass, Fixnum, Bignum, Float 158 | end 159 | end 160 | private_constant :Restrictions 161 | 162 | # Validate the given type/protocol specification. 163 | # 164 | # @param [Hash] types the type specification 165 | # @raise [ArgumentError] when the specification is not valid 166 | def check_types!(types) 167 | return if types.nil? 168 | unless types.all?{|k,v| v.is_a?(Module) || v.is_a?(Symbol) } 169 | raise ArgumentError.new('invalid specification') 170 | end 171 | end 172 | 173 | # Use the given `AbstractStruct` class and build the methods necessary 174 | # to support the given data fields. 175 | # 176 | # @param [String] name the name under which to register the record when given 177 | # @param [Array] fields the list of symbolic names for all data fields 178 | # @return [Functional::AbstractStruct] the record class 179 | def build(name, fields, types, &block) 180 | fields = [name].concat(fields) unless name.nil? 181 | record, fields = AbstractStruct.define_class(self, :record, fields) 182 | record.class_variable_set(:@@restrictions, Restrictions.new(types, &block)) 183 | define_initializer(record) 184 | fields.each do |field| 185 | define_reader(record, field) 186 | end 187 | record 188 | end 189 | 190 | # Define an initializer method on the given record class. 191 | # 192 | # @param [Functional::AbstractStruct] record the new record class 193 | # @return [Functional::AbstractStruct] the record class 194 | def define_initializer(record) 195 | record.send(:define_method, :initialize) do |data = {}| 196 | super() 197 | restrictions = record.class_variable_get(:@@restrictions) 198 | data = record.fields.reduce({}) do |memo, field| 199 | memo[field] = data.fetch(field, restrictions.clone_default(field)) 200 | memo 201 | end 202 | restrictions.validate!(data) 203 | set_data_hash(data) 204 | set_values_array(data.values) 205 | ensure_ivar_visibility! 206 | self.freeze 207 | end 208 | record 209 | end 210 | 211 | # Define a reader method on the given record class for the given data field. 212 | # 213 | # @param [Functional::AbstractStruct] record the new record class 214 | # @param [Symbol] field symbolic name of the current data field 215 | # @return [Functional::AbstractStruct] the record class 216 | def define_reader(record, field) 217 | record.send(:define_method, field) do 218 | to_h[field] 219 | end 220 | record 221 | end 222 | end 223 | end 224 | -------------------------------------------------------------------------------- /spec/functional/protocol_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'protocol specification' do 2 | 3 | before(:each) do 4 | @protocol_info = Functional::Protocol.class_variable_get(:@@info) 5 | Functional::Protocol.class_variable_set(:@@info, {}) 6 | end 7 | 8 | after(:each) do 9 | Functional::Protocol.class_variable_set(:@@info, @protocol_info) 10 | end 11 | 12 | context 'SpecifyProtocol method' do 13 | 14 | context 'without a block' do 15 | 16 | it 'returns the specified protocol when defined' do 17 | Functional::SpecifyProtocol(:Foo){ nil } 18 | expect(Functional::SpecifyProtocol(:Foo)).to_not be_nil 19 | end 20 | 21 | it 'returns nil when not defined' do 22 | expect(Functional::SpecifyProtocol(:Foo)).to be_nil 23 | end 24 | end 25 | 26 | context 'with a block' do 27 | 28 | it 'raises an exception if the protocol has already been specified' do 29 | Functional::SpecifyProtocol(:Foo){ nil } 30 | 31 | expect { 32 | Functional::SpecifyProtocol(:Foo){ nil } 33 | }.to raise_error(Functional::ProtocolError) 34 | end 35 | 36 | it 'returns the specified protocol once defined' do 37 | expect(Functional::SpecifyProtocol(:Foo){ nil }).to be_a Functional::ProtocolInfo 38 | end 39 | end 40 | end 41 | 42 | describe Functional::Protocol do 43 | 44 | context 'Satisfy?' do 45 | 46 | it 'accepts and checks multiple protocols' do 47 | Functional::SpecifyProtocol(:foo){ instance_method(:foo) } 48 | Functional::SpecifyProtocol(:bar){ instance_method(:foo) } 49 | Functional::SpecifyProtocol(:baz){ instance_method(:foo) } 50 | 51 | clazz = Class.new do 52 | def foo(); nil; end 53 | end 54 | 55 | expect( 56 | Functional::Protocol.Satisfy?(clazz.new, :foo, :bar, :baz) 57 | ).to be true 58 | end 59 | 60 | it 'returns false if one or more protocols have not been defined' do 61 | Functional::SpecifyProtocol(:foo){ instance_method(:foo) } 62 | 63 | expect( 64 | Functional::Protocol.Satisfy?('object', :foo, :bar) 65 | ).to be false 66 | end 67 | 68 | it 'raises an exception if no protocols are listed' do 69 | expect { 70 | Functional::Protocol::Satisfy?('object') 71 | }.to raise_error(ArgumentError) 72 | end 73 | 74 | it 'returns true on success' do 75 | Functional::SpecifyProtocol(:foo){ instance_method(:foo) } 76 | 77 | clazz = Class.new do 78 | def foo(); nil; end 79 | end 80 | 81 | expect( 82 | Functional::Protocol.Satisfy?(clazz.new, :foo) 83 | ).to be true 84 | end 85 | 86 | it 'returns false on failure' do 87 | Functional::SpecifyProtocol(:foo) do 88 | instance_method(:foo, 0) 89 | class_method(:bar, 0) 90 | end 91 | 92 | clazz = Class.new do 93 | def foo(); nil; end 94 | end 95 | 96 | expect( 97 | Functional::Protocol.Satisfy?(clazz.new, :foo) 98 | ).to be false 99 | end 100 | 101 | it 'validates classes' do 102 | Functional::SpecifyProtocol(:foo) do 103 | instance_method(:foo) 104 | class_method(:bar) 105 | end 106 | 107 | clazz = Class.new do 108 | def foo(); nil; end 109 | def self.bar(); nil; end 110 | end 111 | 112 | expect( 113 | Functional::Protocol.Satisfy?(clazz, :foo) 114 | ).to be true 115 | end 116 | 117 | it 'validates modules' do 118 | Functional::SpecifyProtocol(:foo) do 119 | instance_method(:foo) 120 | class_method(:bar) 121 | end 122 | 123 | mod = Module.new do 124 | def foo(); nil; end 125 | def self.bar(); nil; end 126 | end 127 | 128 | expect( 129 | Functional::Protocol.Satisfy?(mod, :foo) 130 | ).to be true 131 | end 132 | end 133 | 134 | context 'Satisfy!' do 135 | 136 | it 'accepts and checks multiple protocols' do 137 | Functional::SpecifyProtocol(:foo){ instance_method(:foo) } 138 | Functional::SpecifyProtocol(:bar){ instance_method(:foo) } 139 | Functional::SpecifyProtocol(:baz){ instance_method(:foo) } 140 | 141 | clazz = Class.new do 142 | def foo(); nil; end 143 | end 144 | 145 | target = clazz.new 146 | expect( 147 | Functional::Protocol.Satisfy!(target, :foo, :bar, :baz) 148 | ).to eq target 149 | end 150 | 151 | it 'raises an exception if one or more protocols have not been defined' do 152 | Functional::SpecifyProtocol(:foo){ instance_method(:foo) } 153 | 154 | expect{ 155 | Functional::Protocol.Satisfy!('object', :foo, :bar) 156 | }.to raise_error(Functional::ProtocolError) 157 | end 158 | 159 | it 'raises an exception if no protocols are listed' do 160 | expect { 161 | Functional::Protocol::Satisfy!('object') 162 | }.to raise_error(ArgumentError) 163 | end 164 | 165 | it 'returns the target on success' do 166 | Functional::SpecifyProtocol(:foo){ instance_method(:foo) } 167 | 168 | clazz = Class.new do 169 | def foo(); nil; end 170 | end 171 | 172 | target = clazz.new 173 | expect( 174 | Functional::Protocol.Satisfy!(target, :foo) 175 | ).to eq target 176 | end 177 | 178 | it 'raises an exception on failure' do 179 | Functional::SpecifyProtocol(:foo){ instance_method(:foo) } 180 | 181 | expect{ 182 | Functional::Protocol.Satisfy!('object', :foo) 183 | }.to raise_error(Functional::ProtocolError) 184 | end 185 | 186 | it 'validates classes' do 187 | Functional::SpecifyProtocol(:foo) do 188 | instance_method(:foo) 189 | class_method(:bar) 190 | end 191 | 192 | clazz = Class.new do 193 | def foo(); nil; end 194 | def self.bar(); nil; end 195 | end 196 | 197 | expect{ 198 | Functional::Protocol.Satisfy!(clazz, :foo) 199 | }.to_not raise_exception 200 | end 201 | 202 | it 'validates modules' do 203 | Functional::SpecifyProtocol(:foo) do 204 | instance_method(:foo) 205 | class_method(:bar) 206 | end 207 | 208 | mod = Module.new do 209 | def foo(); nil; end 210 | def self.bar(); nil; end 211 | end 212 | 213 | expect{ 214 | Functional::Protocol.Satisfy!(mod, :foo) 215 | }.to_not raise_exception 216 | end 217 | end 218 | 219 | context 'Specified?' do 220 | 221 | it 'returns true when all protocols have been defined' do 222 | Functional::SpecifyProtocol(:foo){ nil } 223 | Functional::SpecifyProtocol(:bar){ nil } 224 | Functional::SpecifyProtocol(:baz){ nil } 225 | 226 | expect(Functional::Protocol.Specified?(:foo, :bar, :baz)).to be true 227 | end 228 | 229 | it 'returns false when one or more of the protocols have not been defined' do 230 | Functional::SpecifyProtocol(:foo){ nil } 231 | Functional::SpecifyProtocol(:bar){ nil } 232 | 233 | expect(Functional::Protocol.Specified?(:foo, :bar, :baz)).to be false 234 | end 235 | 236 | it 'raises an exception when no protocols are given' do 237 | expect { 238 | Functional::Protocol.Specified? 239 | }.to raise_error(ArgumentError) 240 | end 241 | end 242 | 243 | context 'Specified!' do 244 | 245 | it 'returns true when all protocols have been defined' do 246 | Functional::SpecifyProtocol(:foo){ nil } 247 | Functional::SpecifyProtocol(:bar){ nil } 248 | Functional::SpecifyProtocol(:baz){ nil } 249 | 250 | expect(Functional::Protocol.Specified!(:foo, :bar, :baz)).to be true 251 | expect { 252 | Functional::Protocol.Specified!(:foo, :bar, :baz) 253 | }.to_not raise_error 254 | end 255 | 256 | it 'raises an exception when one or more of the protocols have not been defined' do 257 | Functional::SpecifyProtocol(:foo){ nil } 258 | Functional::SpecifyProtocol(:bar){ nil } 259 | 260 | expect { 261 | Functional::Protocol.Specified!(:foo, :bar, :baz) 262 | }.to raise_error(Functional::ProtocolError) 263 | end 264 | 265 | it 'raises an exception when no protocols are given' do 266 | expect { 267 | Functional::Protocol.Specified! 268 | }.to raise_error(ArgumentError) 269 | end 270 | end 271 | end 272 | end 273 | -------------------------------------------------------------------------------- /lib/functional/final_struct.rb: -------------------------------------------------------------------------------- 1 | require 'functional/final_var' 2 | require 'functional/synchronization' 3 | 4 | module Functional 5 | 6 | # A variation on Ruby's `OpenStruct` in which all fields are "final" (meaning 7 | # that new fields can be arbitrarily added to a `FinalStruct` object but once 8 | # set each field becomes immutable). Additionally, predicate methods exist for 9 | # all fields and these predicates indicate if the field has been set. 10 | # 11 | # There are two ways to initialize a `FinalStruct`: with zero arguments or 12 | # with a `Hash` (or any other object that implements a `to_h` method). The 13 | # only difference in behavior is that a `FinalStruct` initialized with a 14 | # hash will pre-define and pre-populate attributes named for the hash keys 15 | # and with values corresponding to the hash values. 16 | # 17 | # @example Instanciation With No Fields 18 | # bucket = Functional::FinalStruct.new 19 | # 20 | # bucket.foo #=> nil 21 | # bucket.foo? #=> false 22 | # 23 | # bucket.foo = 42 #=> 42 24 | # bucket.foo #=> 42 25 | # bucket.foo? #=> true 26 | # 27 | # bucket.foo = 42 #=> Functional::FinalityError: final accessor 'bar' has already been set 28 | # 29 | # @example Instanciation With a Hash 30 | # name = Functional::FinalStruct.new(first: 'Douglas', last: 'Adams') 31 | # 32 | # name.first #=> 'Douglas' 33 | # name.last #=> 'Adams' 34 | # name.first? #=> true 35 | # name.last? #=> true 36 | # 37 | # name.middle #=> nil 38 | # name.middle? #=> false 39 | # name.middle = 'Noel' #=> 'Noel' 40 | # name.middle? #=> true 41 | # 42 | # name.first = 'Sam' #=> Functional::FinalityError: final accessor 'first' has already been set 43 | # 44 | # @see http://www.ruby-doc.org/stdlib-2.1.2/libdoc/ostruct/rdoc/OpenStruct.html 45 | # @see http://en.wikipedia.org/wiki/Final_(Java) Java `final` keyword 46 | # 47 | # @!macro thread_safe_final_object 48 | class FinalStruct < Synchronization::Object 49 | 50 | # Creates a new `FinalStruct` object. By default, the resulting `FinalStruct` 51 | # object will have no attributes. The optional hash, if given, will generate 52 | # attributes and values (can be a `Hash` or any object with a `to_h` method). 53 | # 54 | # @param [Hash] attributes the field/value pairs to set on creation 55 | def initialize(attributes = {}) 56 | raise ArgumentError.new('attributes must be given as a hash or not at all') unless attributes.respond_to?(:to_h) 57 | super 58 | synchronize do 59 | @attribute_hash = {} 60 | attributes.to_h.each_pair do |field, value| 61 | ns_set_attribute(field, value) 62 | end 63 | end 64 | end 65 | 66 | # @!macro [attach] final_struct_get_method 67 | # 68 | # Get the value of the given field. 69 | # 70 | # @param [Symbol] field the field to retrieve the value for 71 | # @return [Object] the value of the field is set else nil 72 | def get(field) 73 | synchronize { ns_get_attribute(field) } 74 | end 75 | alias_method :[], :get 76 | 77 | # @!macro [attach] final_struct_set_method 78 | # 79 | # Set the value of the give field to the given value. 80 | # 81 | # It is a logical error to attempt to set a `final` field more than once, as this 82 | # violates the concept of finality. Calling the method a second or subsequent time 83 | # for a given field will result in an exception being raised. 84 | # 85 | # @param [Symbol] field the field to set the value for 86 | # @param [Object] value the value to set the field to 87 | # @return [Object] the final value of the given field 88 | # 89 | # @raise [Functional::FinalityError] if the given field has already been set 90 | def set(field, value) 91 | synchronize do 92 | if ns_attribute_has_been_set?(field) 93 | raise FinalityError.new("final accessor '#{field}' has already been set") 94 | else 95 | ns_set_attribute(field, value) 96 | end 97 | end 98 | end 99 | alias_method :[]=, :set 100 | 101 | # @!macro [attach] final_struct_set_predicate 102 | # 103 | # Check the internal hash to unambiguously verify that the given 104 | # attribute has been set. 105 | # 106 | # @param [Symbol] field the field to get the value for 107 | # @return [Boolean] true if the field has been set else false 108 | def set?(field) 109 | synchronize { ns_attribute_has_been_set?(field) } 110 | end 111 | 112 | # Get the current value of the given field if already set else set the value of 113 | # the given field to the given value. 114 | # 115 | # @param [Symbol] field the field to get or set the value for 116 | # @param [Object] value the value to set the field to when not previously set 117 | # @return [Object] the final value of the given field 118 | def get_or_set(field, value) 119 | synchronize { ns_attribute_has_been_set?(field) ? ns_get_attribute(field) : ns_set_attribute(field, value) } 120 | end 121 | 122 | # Get the current value of the given field if already set else return the given 123 | # default value. 124 | # 125 | # @param [Symbol] field the field to get the value for 126 | # @param [Object] default the value to return if the field has not been set 127 | # @return [Object] the value of the given field else the given default value 128 | def fetch(field, default) 129 | synchronize { ns_attribute_has_been_set?(field) ? ns_get_attribute(field) : default } 130 | end 131 | 132 | # Calls the block once for each attribute, passing the key/value pair as parameters. 133 | # If no block is given, an enumerator is returned instead. 134 | # 135 | # @yieldparam [Symbol] field the struct field for the current iteration 136 | # @yieldparam [Object] value the value of the current field 137 | # 138 | # @return [Enumerable] when no block is given 139 | def each_pair 140 | return enum_for(:each_pair) unless block_given? 141 | synchronize do 142 | @attribute_hash.each do |field, value| 143 | yield(field, value) 144 | end 145 | end 146 | end 147 | 148 | # Converts the `FinalStruct` to a `Hash` with keys representing each attribute 149 | # (as symbols) and their corresponding values. 150 | # 151 | # @return [Hash] a `Hash` representing this struct 152 | def to_h 153 | synchronize { @attribute_hash.dup } 154 | end 155 | 156 | # Compares this object and other for equality. A `FinalStruct` is `eql?` to 157 | # other when other is a `FinalStruct` and the two objects have identical 158 | # fields and values. 159 | # 160 | # @param [Object] other the other record to compare for equality 161 | # @return [Boolean] true when equal else false 162 | def eql?(other) 163 | other.is_a?(self.class) && to_h == other.to_h 164 | end 165 | alias_method :==, :eql? 166 | 167 | # Describe the contents of this object in a string. 168 | # 169 | # @return [String] the string representation of this object 170 | # 171 | # @!visibility private 172 | def inspect 173 | state = to_h.to_s.gsub(/^{/, '').gsub(/}$/, '') 174 | "#<#{self.class} #{state}>" 175 | end 176 | alias_method :to_s, :inspect 177 | 178 | protected 179 | 180 | # @!macro final_struct_get_method 181 | # @!visibility private 182 | def ns_get_attribute(field) 183 | @attribute_hash[field.to_sym] 184 | end 185 | 186 | # @!macro final_struct_set_method 187 | # @!visibility private 188 | def ns_set_attribute(field, value) 189 | @attribute_hash[field.to_sym] = value 190 | end 191 | 192 | # @!macro final_struct_set_predicate 193 | # @!visibility private 194 | def ns_attribute_has_been_set?(field) 195 | @attribute_hash.has_key?(field.to_sym) 196 | end 197 | 198 | # Check the method name and args for signatures matching potential 199 | # final attribute reader, writer, and predicate methods. If the signature 200 | # matches a reader or predicate, treat the attribute as unset. If the 201 | # signature matches a writer, attempt to set the new attribute. 202 | # 203 | # @param [Symbol] symbol the name of the called function 204 | # @param [Array] args zero or more arguments 205 | # @return [Object] the result of the proxied method or the `super` call 206 | # 207 | # @!visibility private 208 | def method_missing(symbol, *args) 209 | if args.length == 1 && (match = /([^=]+)=$/.match(symbol)) 210 | set(match[1], args.first) 211 | elsif args.length == 0 && (match = /([^\?]+)\?$/.match(symbol)) 212 | set?(match[1]) 213 | elsif args.length == 0 214 | get(symbol) 215 | else 216 | super 217 | end 218 | end 219 | end 220 | end 221 | -------------------------------------------------------------------------------- /doc/protocol.md: -------------------------------------------------------------------------------- 1 | ### Rationale 2 | 3 | Traditional object orientation implements polymorphism inheritance. The *Is-A* 4 | relationship indicates that one object "is a" instance of another object. 5 | Implicit in this relationship, however, is the concept of [type](http://en.wikipedia.org/wiki/Data_type). 6 | Every Ruby object has a *type*, and that type is the name of its `Class` or 7 | `Module`. The Ruby runtime provides a number of reflective methods that allow 8 | objects to be interrogated for type information. The principal of thses is the 9 | `is_a?` (alias `kind_of`) method defined in class `Object`. 10 | 11 | Unlike many traditional object oriented languages, Ruby is a [dynamically typed](http://en.wikipedia.org/wiki/Dynamic_typingDYNAMIC) 12 | language. Types exist but the runtime is free to cast one type into another 13 | at any time. Moreover, Ruby is a [duck typed](http://en.wikipedia.org/wiki/Duck_typing). 14 | If an object "walks like a duck and quacks like a duck then it must be a duck." 15 | When a method needs called on an object Ruby does not check the type of the object, 16 | it simply checks to see if the requested function exists with the proper 17 | [arity](http://en.wikipedia.org/wiki/Arity) and, if it does, dispatches the call. 18 | The duck type analogue to `is_a?` is `respond_to?`. Thus an object can be interrogated 19 | for its behavior rather than its type. 20 | 21 | Although Ruby offers several methods for reflecting on the behavior of a module/class/object, 22 | such as `method`, `instance_methods`, `const_defined?`, the aforementioned `respond_to?`, 23 | and others, Ruby lacks a convenient way to group collections of methods in any way that 24 | does not involve type. Both modules and classes provide mechanisms for combining 25 | methods into cohesive abstractions, but they both imply type. This is anathema to Ruby's 26 | dynamism and duck typing. What Ruby needs is a way to collect a group of method names 27 | and signatures into a cohesive collection that embraces duck typing and dynamic dispatch. 28 | This is what protocols do. 29 | 30 | ### Specifying 31 | 32 | A "protocol" is a loose collection of method, attribute, and constant names with optional 33 | arity values. The protocol definition does very little on its own. The power of protocols 34 | is that they provide a way for modules, classes, and objects to be interrogated with 35 | respect to common behavior, not common type. At the core a protocol is nothing more 36 | than a collection of `respond_to?` method calls that ask the question "Does this thing 37 | *behave* like this other thing." 38 | 39 | Protocols are specified with the `Functional::SpecifyProtocol` method. It takes one parameter, 40 | the name of the protocol, and a block which contains the protocol specification. This registers 41 | the protocol specification and makes it available for use later when interrogating ojects 42 | for their behavior. 43 | 44 | ##### Defining Attributes, Methods, and Constants 45 | 46 | A single protocol specification can include definition for attributes, methods, 47 | and constants. Methods and attributes can be defined as class/module methods or 48 | as instance methods. Within the a protocol specification each item must include 49 | the symbolic name of the item being defined. 50 | 51 | ```ruby 52 | Functional::SpecifyProtocol(:KitchenSink) do 53 | instance_method :instance_method 54 | class_method :class_method 55 | attr_accessor :attr_accessor 56 | attr_reader :attr_reader 57 | attr_writer :attr_writer 58 | class_attr_accessor :class_attr_accessor 59 | class_attr_reader :class_attr_reader 60 | class_attr_writer :class_attr_writer 61 | constant :CONSTANT 62 | end 63 | ``` 64 | 65 | Definitions for accessors are expanded at specification into the apprporiate 66 | method(s). Which means that this: 67 | 68 | ```ruby 69 | Functional::SpecifyProtocol(:Name) do 70 | attr_accessor :first 71 | attr_accessor :middle 72 | attr_accessor :last 73 | attr_accessor :suffix 74 | end 75 | ``` 76 | 77 | is the same as: 78 | 79 | ```ruby 80 | Functional::SpecifyProtocol(:Name) do 81 | instance_method :first 82 | instance_method :first= 83 | instance_method :middle 84 | instance_method :middle= 85 | instance_method :last 86 | instance_method :last= 87 | instance_method :suffix 88 | instance_method :suffix= 89 | end 90 | ``` 91 | 92 | Protocols only care about the methods themselves, not how they were declared. 93 | 94 | ### Arity 95 | 96 | In addition to defining *which* methods exist, the required method arity can 97 | indicated. Arity is optional. When no arity is given any arity will be expected. 98 | The arity rules follow those defined for the `arity` method of Ruby's 99 | [Method class](http://www.ruby-doc.org/core-2.1.2/Method.htmlmethod-i-arity): 100 | 101 | * Methods with a fixed number of arguments have a non-negative arity 102 | * Methods with optional arguments have an arity `-n - 1`, where n is the number of required arguments 103 | * Methods with a variable number of arguments have an arity of `-1` 104 | 105 | ```ruby 106 | Functional::SpecifyProtocol(:Foo) do 107 | instance_method :any_args 108 | instance_method :no_args, 0 109 | instance_method :three_args, 3 110 | instance_method :optional_args, -2 111 | instance_method :variable_args, -1 112 | end 113 | 114 | class Bar 115 | 116 | def any_args(a, b, c=1, d=2, *args) 117 | end 118 | 119 | def no_args 120 | end 121 | 122 | def three_args(a, b, c) 123 | end 124 | 125 | def optional_args(a, b=1, c=2) 126 | end 127 | 128 | def variable_args(*args) 129 | end 130 | end 131 | ``` 132 | 133 | ### Reflection 134 | 135 | Once a protocol has been defined, any class, method, or object may be interrogated 136 | for adherence to one or more protocol specifications. The methods of the 137 | `Functional::Protocol` classes provide this capability. The `Satisfy?` method 138 | takes a module/class/object as the first parameter and one or more protocol names 139 | as the second and subsequent parameters. It returns a boolean value indicating 140 | whether the given object satisfies the protocol requirements: 141 | 142 | ```ruby 143 | Functional::SpecifyProtocol(:Queue) do 144 | instance_method :push, 1 145 | instance_method :pop, 0 146 | instance_method :length, 0 147 | end 148 | 149 | Functional::SpecifyProtocol(:List) do 150 | instance_method :[]=, 2 151 | instance_method :[], 1 152 | instance_method :each, 0 153 | instance_method :length, 0 154 | end 155 | 156 | Functional::Protocol::Satisfy?(Queue, :Queue) => true 157 | Functional::Protocol::Satisfy?(Queue, :List) => false 158 | 159 | list = [1, 2, 3] 160 | Functional::Protocol::Satisfy?(Array, :List, :Queue) => true 161 | Functional::Protocol::Satisfy?(list, :List, :Queue) => true 162 | 163 | Functional::Protocol::Satisfy?(Hash, :Queue) => false 164 | 165 | Functional::Protocol::Satisfy?('foo bar baz', :List) => false 166 | ``` 167 | 168 | The `Satisfy!` method performs the exact same check but instead raises an exception 169 | when the protocol is not satisfied: 170 | 171 | ``` 172 | 2.1.2 :021 > Functional::Protocol::Satisfy!(Queue, :List) 173 | Functional::ProtocolError: Value (Class) 'Thread::Queue' does not behave as all of: :List. 174 | from /Projects/functional-ruby/lib/functional/protocol.rb:67:in `error' 175 | from /Projects/functional-ruby/lib/functional/protocol.rb:36:in `Satisfy!' 176 | from (irb):21 177 | ... 178 | ``` 179 | The `Functional::Protocol` module can be included within other classes 180 | to eliminate the namespace requirement when calling: 181 | 182 | ```ruby 183 | class MessageFormatter 184 | include Functional::Protocol 185 | 186 | def format(message) 187 | if Satisfy?(message, :Internal) 188 | format_internal_message(message) 189 | elsif Satisfy?(message, :Error) 190 | format_error_message(message) 191 | else 192 | format_generic_message(message) 193 | end 194 | end 195 | 196 | private 197 | 198 | def format_internal_message(message) 199 | format the message... 200 | end 201 | 202 | def format_error_message(message) 203 | format the message... 204 | end 205 | 206 | def format_generic_message(message) 207 | format the message... 208 | end 209 | ``` 210 | 211 | ### Inspiration 212 | 213 | Protocols and similar functionality exist in several other programming languages. 214 | A few languages that provided inspiration for this inplementation are: 215 | 216 | * Clojure [protocol](http://clojure.org/protocols) 217 | * Erlang [behaviours](http://www.erlang.org/doc/design_principles/des_princ.htmlid60128) 218 | * Objective-C [protocol](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithProtocols/WorkingwithProtocols.html) 219 | (and the corresponding Swift [protocol](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html)) 220 | -------------------------------------------------------------------------------- /spec/functional/option_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'abstract_struct_shared' 2 | require 'securerandom' 3 | 4 | module Functional 5 | 6 | describe Option do 7 | 8 | let!(:value){ 42 } 9 | 10 | let!(:expected_fields){ [:some] } 11 | let!(:expected_values){ [value] } 12 | 13 | let(:struct_class) { Option } 14 | let(:struct_object) { Option.some(value) } 15 | let(:other_object) { Option.some(Object.new) } 16 | 17 | let(:some_subject){ Option.some(value) } 18 | let(:none_subject){ Option.none } 19 | 20 | it_should_behave_like :abstract_struct 21 | 22 | specify{ Functional::Protocol::Satisfy! Option, :Option } 23 | specify{ Functional::Protocol::Satisfy! Option, :Disposition } 24 | 25 | let(:some_value){ SecureRandom.uuid } 26 | let(:other_value){ SecureRandom.uuid } 27 | 28 | context 'initialization' do 29 | 30 | it 'cannot be constructed directly' do 31 | expect { 32 | Option.new 33 | }.to raise_error(NameError) 34 | end 35 | 36 | it 'sets the value when constructed by #some' do 37 | expect(Option.some(value).some).to eq value 38 | end 39 | 40 | it 'sets the value to nil when constructed by #none' do 41 | expect(Option.none.some).to be_nil 42 | end 43 | 44 | it 'sets the reason to nil when constructed by #none' do 45 | expect(Option.none.reason).to be_nil 46 | end 47 | 48 | it 'sets the optional reason when constructed by #none' do 49 | reason = 'foobar' 50 | expect(Option.none(reason).reason).to eq reason 51 | end 52 | 53 | it 'freezes the new object' do 54 | expect(Option.some(:foo)).to be_frozen 55 | expect(Option.none).to be_frozen 56 | end 57 | end 58 | 59 | context 'state' do 60 | 61 | specify '#some? returns true when the some value is set' do 62 | expect(some_subject).to be_some 63 | end 64 | 65 | specify '#some? returns false when none' do 66 | expect(none_subject).to_not be_some 67 | end 68 | 69 | specify '#none? returns true when none' do 70 | expect(none_subject).to be_none 71 | end 72 | 73 | specify '#none? returns false when the some value is set' do 74 | expect(some_subject).to_not be_none 75 | end 76 | 77 | specify '#some returns the some value when some is set' do 78 | expect(some_subject.some).to eq value 79 | end 80 | 81 | specify '#some returns nil when none is set' do 82 | expect(none_subject.some).to be_nil 83 | end 84 | 85 | it 'aliases #some? as #fulfilled?' do 86 | expect(some_subject).to be_fulfilled 87 | expect(none_subject).to_not be_fulfilled 88 | end 89 | 90 | it 'aliases #some? as #value?' do 91 | expect(some_subject).to be_value 92 | expect(none_subject).to_not be_value 93 | end 94 | 95 | it 'aliases #none? as #rejected?' do 96 | expect(some_subject).to_not be_rejected 97 | expect(none_subject).to be_rejected 98 | end 99 | 100 | it 'aliases #none? as #reason?' do 101 | expect(some_subject).to_not be_reason 102 | expect(none_subject).to be_reason 103 | end 104 | 105 | it 'aliases #some as #value' do 106 | expect(some_subject.value).to eq value 107 | expect(none_subject.value).to be_nil 108 | end 109 | 110 | specify '#reason returns nil when some' do 111 | expect(some_subject.reason).to be_nil 112 | end 113 | end 114 | 115 | context 'length' do 116 | 117 | it 'returns 1 when some' do 118 | expect(Option.some(:foo).length).to eq 1 119 | end 120 | 121 | it 'returns 0 when none' do 122 | expect(Option.none.length).to eq 0 123 | end 124 | 125 | it 'as aliased as #size' do 126 | expect(Option.some(:foo).size).to eq 1 127 | expect(Option.none.size).to eq 0 128 | end 129 | end 130 | 131 | context '#and' do 132 | 133 | it 'returns false when none' do 134 | expect(Option.none.and(true)).to be false 135 | end 136 | 137 | it 'returns true when some and other is a some Option' do 138 | other = Option.some(42) 139 | expect(Option.some(:foo).and(other)).to be true 140 | end 141 | 142 | it 'returns false when some and other is a none Option' do 143 | other = Option.none 144 | expect(Option.some(:foo).and(other)).to be false 145 | end 146 | 147 | it 'passes the value to the given block when some' do 148 | expected = false 149 | other = ->(some){ expected = some } 150 | Option.some(42).and(&other) 151 | expect(expected).to eq 42 152 | end 153 | 154 | it 'returns true when some and the block returns a truthy value' do 155 | other = ->(some){ 'truthy' } 156 | expect(Option.some(42).and(&other)).to be true 157 | end 158 | 159 | it 'returns false when some and the block returns a falsey value' do 160 | other = ->(some){ nil } 161 | expect(Option.some(42).and(&other)).to be false 162 | end 163 | 164 | it 'returns true when some and given a truthy value' do 165 | expect(Option.some(42).and('truthy')).to be true 166 | end 167 | 168 | it 'returns false when some and given a falsey value' do 169 | expect(Option.some(42).and(nil)).to be false 170 | end 171 | 172 | it 'raises an exception when given both a value and a block' do 173 | expect { 174 | Option.some(42).and(:foo){|some| :bar } 175 | }.to raise_error(ArgumentError) 176 | end 177 | end 178 | 179 | context '#or' do 180 | 181 | it 'returns true when some' do 182 | expect(Option.some(42).or(nil)).to be true 183 | end 184 | 185 | it 'returns true when none and other is a some Option' do 186 | other = Option.some(42) 187 | expect(Option.none.or(other)).to be true 188 | end 189 | 190 | it 'returns false when none and other is a none Option' do 191 | other = Option.none 192 | expect(Option.none.or(other)).to be false 193 | end 194 | 195 | it 'returns true when none and the block returns a truthy value' do 196 | other = ->{ 42 } 197 | expect(Option.none.or(&other)).to be true 198 | end 199 | 200 | it 'returns false when none and the block returns a falsey value' do 201 | other = ->{ false } 202 | expect(Option.none.or(&other)).to be false 203 | end 204 | 205 | it 'returns true when none and given a truthy value' do 206 | expect(Option.none.or('truthy')).to be true 207 | end 208 | 209 | it 'returns false when none and given a falsey value' do 210 | expect(Option.none.or(nil)).to be false 211 | end 212 | 213 | it 'raises an exception when given both a value and a block' do 214 | expect { 215 | Option.none.and(:foo){ :bar } 216 | }.to raise_error(ArgumentError) 217 | end 218 | end 219 | 220 | context '#else' do 221 | 222 | it 'returns the value when some' do 223 | expect(Option.some(some_value).else(other_value)).to eq some_value 224 | end 225 | 226 | it 'returns the given value when none' do 227 | expect(Option.none.else(other_value)).to eq other_value 228 | end 229 | 230 | it 'returns the other value when none and given a some Option' do 231 | other = Option.some(other_value) 232 | expect(Option.none.else(other)).to eq other_value 233 | end 234 | 235 | it 'returns nil when none and given a none Option' do 236 | other = Option.none 237 | expect(Option.none.else(other)).to be_nil 238 | end 239 | 240 | it 'returns the result of the given block when none' do 241 | other = ->{ other_value } 242 | expect(Option.none.else(&other)).to eq other_value 243 | end 244 | 245 | it 'raises an exception when given both a value and a block' do 246 | expect { 247 | Option.none.else(:foo){ :bar } 248 | }.to raise_error(ArgumentError) 249 | end 250 | end 251 | 252 | context '#iff' do 253 | 254 | it 'returns a some option with the given value when the boolean is true' do 255 | subject = Option.iff(:foo, true) 256 | expect(subject).to be_some 257 | expect(subject.some).to eq :foo 258 | end 259 | 260 | it 'returns a none option when the boolean is false' do 261 | subject = Option.iff(:foo, false) 262 | expect(subject).to be_none 263 | expect(subject.some).to be_nil 264 | end 265 | 266 | it 'returns a some option with the given value when the block is truthy' do 267 | subject = Option.iff(:foo){ :baz } 268 | expect(subject).to be_some 269 | expect(subject.some).to eq :foo 270 | end 271 | 272 | it 'returns a none option when the block is false' do 273 | subject = Option.iff(:foo){ false } 274 | expect(subject).to be_none 275 | expect(subject.some).to be_nil 276 | end 277 | 278 | it 'returns a none option when the block is nil' do 279 | subject = Option.iff(:foo){ nil } 280 | expect(subject).to be_none 281 | expect(subject.some).to be_nil 282 | end 283 | 284 | it 'raises an exception when both a boolean and a block are given' do 285 | expect { 286 | subject = Option.iff(:foo, true){ nil } 287 | }.to raise_error(ArgumentError) 288 | end 289 | end 290 | end 291 | end 292 | -------------------------------------------------------------------------------- /spec/functional/final_struct_spec.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | module Functional 4 | 5 | describe FinalStruct do 6 | 7 | context 'instanciation' do 8 | 9 | specify 'with no args defines no fields' do 10 | subject = FinalStruct.new 11 | expect(subject.to_h).to be_empty 12 | end 13 | 14 | specify 'with a hash sets fields using has values' do 15 | subject = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three') 16 | expect(subject.foo).to eq 1 17 | expect(subject.bar).to eq :two 18 | expect(subject.baz).to eq 'three' 19 | end 20 | 21 | specify 'with a hash creates true predicates for has keys' do 22 | subject = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three') 23 | expect(subject.foo?).to be true 24 | expect(subject.bar?).to be true 25 | expect(subject.baz?).to be true 26 | end 27 | 28 | specify 'can be created from any object that responds to #to_h' do 29 | clazz = Class.new do 30 | def to_h; {answer: 42, harmless: 'mostly'}; end 31 | end 32 | struct = clazz.new 33 | subject = FinalStruct.new(struct) 34 | expect(subject.answer).to eq 42 35 | expect(subject.harmless).to eq 'mostly' 36 | end 37 | 38 | specify 'raises an exception if given a non-hash argument' do 39 | expect { 40 | FinalStruct.new(:bogus) 41 | }.to raise_error(ArgumentError) 42 | end 43 | end 44 | 45 | context 'set fields' do 46 | 47 | subject do 48 | struct = FinalStruct.new 49 | struct.foo = 42 50 | struct.bar = "Don't Panic" 51 | struct 52 | end 53 | 54 | specify 'have a reader which returns the value' do 55 | expect(subject.foo).to eq 42 56 | expect(subject.bar).to eq "Don't Panic" 57 | end 58 | 59 | specify 'have a predicate which returns true' do 60 | expect(subject.foo?).to be true 61 | expect(subject.bar?).to be true 62 | end 63 | 64 | specify 'raise an exception when written to again' do 65 | expect {subject.foo = 0}.to raise_error(Functional::FinalityError) 66 | expect {subject.bar = 0}.to raise_error(Functional::FinalityError) 67 | end 68 | end 69 | 70 | context 'unset fields' do 71 | 72 | subject { FinalStruct.new } 73 | 74 | specify 'have a magic reader that always returns nil' do 75 | expect(subject.foo).to be nil 76 | expect(subject.bar).to be nil 77 | expect(subject.baz).to be nil 78 | end 79 | 80 | specify 'have a magic predicate that always returns false' do 81 | expect(subject.foo?).to be false 82 | expect(subject.bar?).to be false 83 | expect(subject.baz?).to be false 84 | end 85 | 86 | specify 'have a magic writer that sets the field' do 87 | expect(subject.foo = 42).to eq 42 88 | expect(subject.bar = :towel).to eq :towel 89 | expect(subject.baz = "Don't Panic").to eq "Don't Panic" 90 | end 91 | end 92 | 93 | context 'accessors' do 94 | 95 | let!(:field_value_pairs) { {foo: 1, bar: :two, baz: 'three'} } 96 | 97 | subject { FinalStruct.new(field_value_pairs) } 98 | 99 | specify '#get returns the value of a set field' do 100 | expect(subject.get(:foo)).to eq 1 101 | end 102 | 103 | specify '#get returns nil for an unset field' do 104 | expect(subject.get(:bogus)).to be nil 105 | end 106 | 107 | specify '#[] is an alias for #get' do 108 | expect(subject[:foo]).to eq 1 109 | expect(subject[:bogus]).to be nil 110 | end 111 | 112 | specify '#set sets the value of an unset field' do 113 | subject.set(:harmless, 'mostly') 114 | expect(subject.harmless).to eq 'mostly' 115 | expect(subject.harmless?).to be true 116 | end 117 | 118 | specify '#set raises an exception if the field has already been set' do 119 | subject.set(:harmless, 'mostly') 120 | expect { 121 | subject.set(:harmless, 'extremely') 122 | }.to raise_error(Functional::FinalityError) 123 | end 124 | 125 | specify '#[]= is an alias for set' do 126 | subject[:harmless] = 'mostly' 127 | expect(subject.harmless).to eq 'mostly' 128 | expect { 129 | subject[:harmless] = 'extremely' 130 | }.to raise_error(Functional::FinalityError) 131 | end 132 | 133 | specify '#set? returns false for an unset field' do 134 | expect(subject.set?(:harmless)).to be false 135 | end 136 | 137 | specify '#set? returns true for a field that has been set' do 138 | subject.set(:harmless, 'mostly') 139 | expect(subject.set?(:harmless)).to be true 140 | end 141 | 142 | specify '#get_or_set returns the value of a set field' do 143 | subject.answer = 42 144 | expect(subject.get_or_set(:answer, 100)).to eq 42 145 | end 146 | 147 | specify '#get_or_set sets the value of an unset field' do 148 | subject.get_or_set(:answer, 42) 149 | expect(subject.answer).to eq 42 150 | expect(subject.answer?).to be true 151 | end 152 | 153 | specify '#get_or_set returns the value of a newly set field' do 154 | expect(subject.get_or_set(:answer, 42)).to eq 42 155 | end 156 | 157 | specify '#fetch gets the value of a set field' do 158 | subject.harmless = 'mostly' 159 | expect(subject.fetch(:harmless, 'extremely')).to eq 'mostly' 160 | end 161 | 162 | specify '#fetch returns the given value when the field is unset' do 163 | expect(subject.fetch(:harmless, 'extremely')).to eq 'extremely' 164 | end 165 | 166 | specify '#fetch does not set an unset field' do 167 | subject.fetch(:answer, 42) 168 | expect(subject.answer).to be_nil 169 | expect(subject.answer?).to be false 170 | end 171 | 172 | specify '#to_h returns the key/value pairs for all set values' do 173 | subject = FinalStruct.new(field_value_pairs) 174 | expect(subject.to_h).to eq field_value_pairs 175 | end 176 | 177 | specify '#to_h is updated when new fields are added' do 178 | subject = FinalStruct.new 179 | field_value_pairs.each_pair do |field, value| 180 | subject.set(field, value) 181 | end 182 | expect(subject.to_h).to eq field_value_pairs 183 | end 184 | 185 | specify '#each_pair returns an Enumerable when no block given' do 186 | subject = FinalStruct.new(field_value_pairs) 187 | expect(subject.each_pair).to be_a Enumerable 188 | end 189 | 190 | specify '#each_pair enumerates over each field/value pair' do 191 | subject = FinalStruct.new(field_value_pairs) 192 | result = {} 193 | 194 | subject.each_pair do |field, value| 195 | result[field] = value 196 | end 197 | 198 | expect(result).to eq field_value_pairs 199 | end 200 | end 201 | 202 | context 'reflection' do 203 | 204 | specify '#eql? returns true when both define the same fields with the same values' do 205 | first = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three') 206 | second = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three') 207 | 208 | expect(first.eql?(second)).to be true 209 | expect(first == second).to be true 210 | end 211 | 212 | specify '#eql? returns false when other has different fields defined' do 213 | first = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three') 214 | second = FinalStruct.new(foo: 1, 'bar' => :two) 215 | 216 | expect(first.eql?(second)).to be false 217 | expect(first == second).to be false 218 | end 219 | 220 | specify '#eql? returns false when other has different field values' do 221 | first = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three') 222 | second = FinalStruct.new(foo: 1, 'bar' => :two, baz: 3) 223 | 224 | expect(first.eql?(second)).to be false 225 | expect(first == second).to be false 226 | end 227 | 228 | specify '#eql? returns false when other is not a FinalStruct' do 229 | attributes = {answer: 42, harmless: 'mostly'} 230 | clazz = Class.new do 231 | def to_h; {answer: 42, harmless: 'mostly'}; end 232 | end 233 | other = clazz.new 234 | subject = FinalStruct.new(attributes) 235 | expect(subject.eql?(other)).to be false 236 | expect(subject == other).to be false 237 | end 238 | 239 | specify '#inspect begins with the class name' do 240 | subject = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three') 241 | expect(subject.inspect).to match(/^#<#{described_class}\s+/) 242 | end 243 | 244 | specify '#inspect includes all field/value pairs' do 245 | field_value_pairs = {foo: 1, 'bar' => :two, baz: 'three'} 246 | subject = FinalStruct.new(field_value_pairs) 247 | 248 | field_value_pairs.each do |field, value| 249 | expect(subject.inspect).to match(/:#{field}=>"?:?#{value}"?/) 250 | end 251 | end 252 | 253 | specify '#to_s returns the same value as #inspect' do 254 | subject = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three') 255 | expect(subject.to_s).to eq subject.inspect 256 | end 257 | 258 | specify '#method_missing raises an exception for methods with unrecognized signatures' do 259 | expect { 260 | subject.foo(1, 2, 3) 261 | }.to raise_error(NoMethodError) 262 | end 263 | end 264 | end 265 | end 266 | -------------------------------------------------------------------------------- /lib/functional/either.rb: -------------------------------------------------------------------------------- 1 | require 'functional/abstract_struct' 2 | require 'functional/protocol' 3 | require 'functional/synchronization' 4 | 5 | Functional::SpecifyProtocol(:Either) do 6 | instance_method :left, 0 7 | instance_method :left?, 0 8 | instance_method :right, 0 9 | instance_method :right?, 0 10 | end 11 | 12 | module Functional 13 | 14 | # The `Either` type represents a value of one of two possible types (a 15 | # disjoint union). It is an immutable structure that contains one and only one 16 | # value. That value can be stored in one of two virtual position, `left` or 17 | # `right`. The position provides context for the encapsulated data. 18 | # 19 | # One of the main uses of `Either` is as a return value that can indicate 20 | # either success or failure. Object oriented programs generally report errors 21 | # through either state or exception handling, neither of which work well in 22 | # functional programming. In the former case, a method is called on an object 23 | # and when an error occurs the state of the object is updated to reflect the 24 | # error. This does not translate well to functional programming because they 25 | # eschew state and mutable objects. In the latter, an exception handling block 26 | # provides branching logic when an exception is thrown. This does not 27 | # translate well to functional programming because it eschews side effects 28 | # like structured exception handling (and structured exception handling tends 29 | # to be very expensive). `Either` provides a powerful and easy-to-use 30 | # alternative. 31 | # 32 | # A function that may generate an error can choose to return an immutable 33 | # `Either` object in which the position of the value (left or right) indicates 34 | # the nature of the data. By convention, a `left` value indicates an error and 35 | # a `right` value indicates success. This leaves the caller with no ambiguity 36 | # regarding success or failure, requires no persistent state, and does not 37 | # require expensive exception handling facilities. 38 | # 39 | # `Either` provides several aliases and convenience functions to facilitate 40 | # these failure/success conventions. The `left` and `right` functions, 41 | # including their derivatives, are mirrored by `reason` and `value`. Failure 42 | # is indicated by the presence of a `reason` and success is indicated by the 43 | # presence of a `value`. When an operation has failed the either is in a 44 | # `rejected` state, and when an operation has successed the either is in a 45 | # `fulfilled` state. A common convention is to use a Ruby `Exception` as the 46 | # `reason`. The factory method `error` facilitates this. The semantics and 47 | # conventions of `reason`, `value`, and their derivatives follow the 48 | # conventions of the Concurrent Ruby gem. 49 | # 50 | # The `left`/`right` and `reason`/`value` methods are not mutually exclusive. 51 | # They can be commingled and still result in functionally correct code. This 52 | # practice should be avoided, however. Consistent use of either `left`/`right` 53 | # or `reason`/`value` against each `Either` instance will result in more 54 | # expressive, intent-revealing code. 55 | # 56 | # @example 57 | # 58 | # require 'uri' 59 | # 60 | # def web_host(url) 61 | # uri = URI(url) 62 | # if uri.scheme != 'http' 63 | # Functional::Either.left('Invalid HTTP URL') 64 | # else 65 | # Functional::Either.right(uri.host) 66 | # end 67 | # end 68 | # 69 | # good = web_host('http://www.concurrent-ruby.com') 70 | # good.right? #=> true 71 | # good.right #=> "www.concurrent-ruby" 72 | # good.left #=> nil 73 | # 74 | # good = web_host('bogus') 75 | # good.right? #=> false 76 | # good.right #=> nil 77 | # good.left #=> "Invalid HTTP URL" 78 | # 79 | # @see http://functionaljava.googlecode.com/svn/artifacts/3.0/javadoc/fj/data/Either.html Functional Java 80 | # @see https://hackage.haskell.org/package/base-4.2.0.1/docs/Data-Either.html Haskell Data.Either 81 | # @see http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Obligation.html Concurrent Ruby 82 | # 83 | # @!macro thread_safe_immutable_object 84 | class Either < Synchronization::Object 85 | include AbstractStruct 86 | 87 | self.datatype = :either 88 | self.fields = [:left, :right].freeze 89 | 90 | # @!visibility private 91 | NO_VALUE = Object.new.freeze 92 | 93 | private_class_method :new 94 | 95 | class << self 96 | 97 | # Construct a left value of either. 98 | # 99 | # @param [Object] value The value underlying the either. 100 | # @return [Either] A new either with the given left value. 101 | def left(value) 102 | new(value, true).freeze 103 | end 104 | alias_method :reason, :left 105 | 106 | # Construct a right value of either. 107 | # 108 | # @param [Object] value The value underlying the either. 109 | # @return [Either] A new either with the given right value. 110 | def right(value) 111 | new(value, false).freeze 112 | end 113 | alias_method :value, :right 114 | 115 | # Create an `Either` with the left value set to an `Exception` object 116 | # complete with message and backtrace. This is a convenience method for 117 | # supporting the reason/value convention with the reason always being 118 | # an `Exception` object. When no exception class is given `StandardError` 119 | # will be used. When no message is given the default message for the 120 | # given error class will be used. 121 | # 122 | # @example 123 | # 124 | # either = Functional::Either.error("You're a bad monkey, Mojo Jojo") 125 | # either.fulfilled? #=> false 126 | # either.rejected? #=> true 127 | # either.value #=> nil 128 | # either.reason #=> # 129 | # 130 | # @param [String] message The message for the new error object. 131 | # @param [Exception] clazz The class for the new error object. 132 | # @return [Either] A new either with an error object as the left value. 133 | def error(message = nil, clazz = StandardError) 134 | ex = clazz.new(message) 135 | ex.set_backtrace(caller) 136 | left(ex) 137 | end 138 | end 139 | 140 | # Projects this either as a left. 141 | # 142 | # @return [Object] The left value or `nil` when `right`. 143 | def left 144 | left? ? to_h[:left] : nil 145 | end 146 | alias_method :reason, :left 147 | 148 | # Projects this either as a right. 149 | # 150 | # @return [Object] The right value or `nil` when `left`. 151 | def right 152 | right? ? to_h[:right] : nil 153 | end 154 | alias_method :value, :right 155 | 156 | # Returns true if this either is a left, false otherwise. 157 | # 158 | # @return [Boolean] `true` if this either is a left, `false` otherwise. 159 | def left? 160 | @is_left 161 | end 162 | alias_method :reason?, :left? 163 | alias_method :rejected?, :left? 164 | 165 | # Returns true if this either is a right, false otherwise. 166 | # 167 | # @return [Boolean] `true` if this either is a right, `false` otherwise. 168 | def right? 169 | ! left? 170 | end 171 | alias_method :value?, :right? 172 | alias_method :fulfilled?, :right? 173 | 174 | # If this is a left, then return the left value in right, or vice versa. 175 | # 176 | # @return [Either] The value of this either swapped to the opposing side. 177 | def swap 178 | if left? 179 | self.class.send(:new, left, false) 180 | else 181 | self.class.send(:new, right, true) 182 | end 183 | end 184 | 185 | # The catamorphism for either. Folds over this either breaking into left or right. 186 | # 187 | # @param [Proc] lproc The function to call if this is left. 188 | # @param [Proc] rproc The function to call if this is right. 189 | # @return [Object] The reduced value. 190 | def either(lproc, rproc) 191 | left? ? lproc.call(left) : rproc.call(right) 192 | end 193 | 194 | # If the condition satisfies, return the given A in left, otherwise, return the given B in right. 195 | # 196 | # @param [Object] lvalue The left value to use if the condition satisfies. 197 | # @param [Object] rvalue The right value to use if the condition does not satisfy. 198 | # @param [Boolean] condition The condition to test (when no block given). 199 | # @yield The condition to test (when no condition given). 200 | # 201 | # @return [Either] A constructed either based on the given condition. 202 | # 203 | # @raise [ArgumentError] When both a condition and a block are given. 204 | def self.iff(lvalue, rvalue, condition = NO_VALUE) 205 | raise ArgumentError.new('requires either a condition or a block, not both') if condition != NO_VALUE && block_given? 206 | condition = block_given? ? yield : !! condition 207 | condition ? left(lvalue) : right(rvalue) 208 | end 209 | 210 | private 211 | 212 | # Create a new Either wil the given value and disposition. 213 | # 214 | # @param [Object] value the value of this either 215 | # @param [Boolean] is_left is this a left either or right? 216 | # 217 | # @!visibility private 218 | def initialize(value, is_left) 219 | super 220 | @is_left = is_left 221 | hsh = is_left ? {left: value, right: nil} : {left: nil, right: value} 222 | set_data_hash(hsh) 223 | set_values_array(hsh.values) 224 | ensure_ivar_visibility! 225 | end 226 | end 227 | end 228 | -------------------------------------------------------------------------------- /spec/functional/record_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'abstract_struct_shared' 2 | require 'securerandom' 3 | 4 | module Functional 5 | 6 | describe Record do 7 | 8 | let!(:expected_fields){ [:a, :b, :c] } 9 | let!(:expected_values){ [42, nil, nil] } 10 | 11 | let(:struct_class) { Record.new(*expected_fields) } 12 | let(:struct_object) { struct_class.new(struct_class.fields.first => 42) } 13 | let(:other_object) { struct_class.new(struct_class.fields.first => Object.new) } 14 | 15 | it_should_behave_like :abstract_struct 16 | 17 | context 'definition' do 18 | 19 | it 'does not register a new class when no name is given' do 20 | Record.new(:foo, :bar, :baz) 21 | expect(defined?(Record::Foo)).to be_falsey 22 | end 23 | 24 | it 'creates a new class when given an array of field names' do 25 | clazz = Record.new(:foo, :bar, :baz) 26 | expect(clazz).to be_a Class 27 | expect(clazz.ancestors).to include(Functional::AbstractStruct) 28 | end 29 | 30 | it 'registers the new class with Record when given a string name and an array' do 31 | Record.new('Bar', :foo, :bar, :baz) 32 | expect(defined?(Record::Bar)).to eq 'constant' 33 | end 34 | 35 | it 'creates a new class when given a hash of field names and types/protocols' do 36 | clazz = Record.new(foo: String, bar: String, baz: String) 37 | expect(clazz).to be_a Class 38 | expect(clazz.ancestors).to include(Functional::AbstractStruct) 39 | end 40 | 41 | it 'registers the new class with Record when given a string name and a hash' do 42 | Record.new('Boom', foo: String, bar: String, baz: String) 43 | expect(defined?(Record::Boom)).to eq 'constant' 44 | end 45 | 46 | it 'raises an exception when given a hash with an invalid type/protocol' do 47 | expect { 48 | Record.new(foo: 'String', bar: String, baz: String) 49 | }.to raise_error(ArgumentError) 50 | end 51 | 52 | it 'raises an exception when given an invalid definition' do 53 | expect { 54 | Record.new(:foo, bar: String, baz: String) 55 | }.to raise_error(ArgumentError) 56 | end 57 | end 58 | 59 | context 'initialization' do 60 | 61 | it 'sets all fields values to nil' do 62 | fields = [:foo, :bar, :baz] 63 | clazz = Record.new(*fields) 64 | 65 | record = clazz.new 66 | 67 | fields.each do |field| 68 | expect(record.send(field)).to be_nil 69 | end 70 | end 71 | 72 | it 'sets initial values based on values given at object construction' do 73 | clazz = Record.new(:foo, :bar, :baz) 74 | record = clazz.new(foo: 1, bar: 2, baz: 3) 75 | 76 | expect(record.foo).to eq 1 77 | expect(record.bar).to eq 2 78 | expect(record.baz).to eq 3 79 | end 80 | 81 | context 'with default values' do 82 | 83 | it 'defaults fields to values given during class creation' do 84 | clazz = Record.new(:foo, :bar, :baz) do 85 | default :foo, 42 86 | default :bar, 'w00t!' 87 | end 88 | 89 | record = clazz.new 90 | expect(record.foo).to eq 42 91 | expect(record.bar).to eq 'w00t!' 92 | expect(record.baz).to be_nil 93 | end 94 | 95 | it 'overrides default values with values provided at object construction' do 96 | clazz = Record.new(:foo, :bar, :baz) do 97 | default :foo, 42 98 | default :bar, 'w00t!' 99 | default :baz, :bogus 100 | end 101 | 102 | record = clazz.new(foo: 1, bar: 2) 103 | 104 | expect(record.foo).to eq 1 105 | expect(record.bar).to eq 2 106 | expect(record.baz).to eq :bogus 107 | end 108 | 109 | it 'duplicates default values when assigning to a new object' do 110 | original = 'Foo' 111 | clazz = Record.new(:foo, :bar, :baz) do 112 | default :foo, original 113 | end 114 | 115 | record = clazz.new 116 | expect(record.foo).to eq original 117 | expect(record.foo.object_id).to_not eql original.object_id 118 | end 119 | 120 | it 'does not conflate defaults across record classes' do 121 | clazz_foo = Record.new(:foo, :bar, :baz) do 122 | default :foo, 42 123 | end 124 | 125 | clazz_matz = Record.new(:foo, :bar, :baz) do 126 | default :foo, 'Matsumoto' 127 | end 128 | 129 | expect(clazz_foo.new.foo).to eq 42 130 | expect(clazz_matz.new.foo).to eq 'Matsumoto' 131 | end 132 | end 133 | 134 | context 'with mandatory fields' do 135 | 136 | it 'raises an exception when values for requred field are not provided' do 137 | clazz = Record.new(:foo, :bar, :baz) do 138 | mandatory :foo 139 | end 140 | 141 | expect { 142 | clazz.new(bar: 1) 143 | }.to raise_exception(ArgumentError) 144 | end 145 | 146 | it 'raises an exception when required values are nil' do 147 | clazz = Record.new(:foo, :bar, :baz) do 148 | mandatory :foo 149 | end 150 | 151 | expect { 152 | clazz.new(foo: nil, bar: 1) 153 | }.to raise_exception(ArgumentError) 154 | end 155 | 156 | it 'allows multiple required fields to be specified together' do 157 | clazz = Record.new(:foo, :bar, :baz) do 158 | mandatory :foo, :bar, :baz 159 | end 160 | 161 | expect { 162 | clazz.new(foo: 1, bar: 2) 163 | }.to raise_exception(ArgumentError) 164 | 165 | expect { 166 | clazz.new(bar: 2, baz: 3) 167 | }.to raise_exception(ArgumentError) 168 | 169 | expect { 170 | clazz.new(foo: 1, bar: 2, baz: 3) 171 | }.to_not raise_exception 172 | end 173 | 174 | it 'does not conflate default values across record classes' do 175 | clazz_foo = Record.new(:foo, :bar, :baz) do 176 | mandatory :foo 177 | end 178 | 179 | clazz_baz = Record.new(:foo, :bar, :baz) do 180 | mandatory :baz 181 | end 182 | 183 | expect { 184 | clazz_foo.new(foo: 42) 185 | }.to_not raise_error 186 | 187 | expect { 188 | clazz_baz.new(baz: 42) 189 | }.to_not raise_error 190 | end 191 | end 192 | 193 | context 'with field type specification' do 194 | 195 | let(:type_safe_definition) do 196 | {foo: String, bar: Fixnum, baz: protocol} 197 | end 198 | 199 | let(:protocol){ SecureRandom.uuid.to_sym } 200 | 201 | let(:clazz_with_protocol) do 202 | Class.new do 203 | def foo() nil end 204 | end 205 | end 206 | 207 | let(:record_clazz) do 208 | Record.new(type_safe_definition) 209 | end 210 | 211 | before(:each) do 212 | Functional::SpecifyProtocol(protocol){ instance_method(:foo) } 213 | end 214 | 215 | it 'raises an exception for a value with an invalid type' do 216 | expect { 217 | record_clazz.new(foo: 'foo', bar: 'bar', baz: clazz_with_protocol.new) 218 | }.to raise_error(ArgumentError) 219 | end 220 | 221 | it 'raises an exception for a value that does not satisfy a protocol' do 222 | expect { 223 | record_clazz.new(foo: 'foo', bar: 42, baz: 'baz') 224 | }.to raise_error(ArgumentError) 225 | end 226 | 227 | it 'creates the object when all values match the appropriate types and protocols' do 228 | record = record_clazz.new(foo: 'foo', bar: 42, baz: clazz_with_protocol.new) 229 | expect(record).to be_a record_clazz 230 | end 231 | end 232 | 233 | it 'allows a field to be required and have a default value' do 234 | clazz = Record.new(:foo, :bar, :baz) do 235 | mandatory :foo 236 | default :foo, 42 237 | end 238 | 239 | expect { 240 | clazz.new 241 | }.to_not raise_exception 242 | 243 | expect(clazz.new.foo).to eq 42 244 | end 245 | 246 | it 'raises an exception if the default value for a require field is nil' do 247 | clazz = Record.new(:foo, :bar, :baz) do 248 | mandatory :foo 249 | default :foo, nil 250 | end 251 | 252 | expect { 253 | clazz.new 254 | }.to raise_exception(ArgumentError) 255 | end 256 | end 257 | 258 | context 'subclassing' do 259 | 260 | specify 'supports all capabilities on subclasses' do 261 | record_clazz = Functional::Record.new(:first, :middle, :last, :suffix) do 262 | mandatory :first, :last 263 | end 264 | 265 | clazz = Class.new(record_clazz) do 266 | def full_name 267 | "#{first} #{last}" 268 | end 269 | 270 | def formal_name 271 | name = [first, middle, last].select{|s| ! s.to_s.empty?}.join(' ') 272 | suffix.to_s.empty? ? name : name + ", #{suffix}" 273 | end 274 | end 275 | 276 | jerry = clazz.new(first: 'Jerry', last: "D'Antonio") 277 | ted = clazz.new(first: 'Ted', middle: 'Theodore', last: 'Logan', suffix: 'Esq.') 278 | 279 | expect(jerry.full_name).to eq "Jerry D'Antonio" 280 | expect(jerry.formal_name).to eq "Jerry D'Antonio" 281 | 282 | expect(ted.full_name).to eq "Ted Logan" 283 | expect(ted.formal_name).to eq "Ted Theodore Logan, Esq." 284 | end 285 | end 286 | end 287 | end 288 | --------------------------------------------------------------------------------