├── .gitignore ├── .rspec ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── arrows.gemspec ├── lib ├── arrows.rb └── arrows │ ├── either.rb │ ├── proc.rb │ └── version.rb ├── pics ├── compose.mermaid ├── compose.mermaid.png ├── concurrent.mermaid ├── concurrent.mermaid.png ├── fanout.mermaid ├── fanout.mermaid.png ├── feedback.mermaid ├── feedback.mermaid.png ├── fmap.mermaid ├── fmap.mermaid.png ├── fork.mermaid └── fork.mermaid.png ├── screenshot.png └── spec ├── arrows ├── functor_spec.rb └── proc_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in arrows.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Thomas Chen 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arrows 2 | 3 | A library to bring some of the best elements of Haskell functional programming into Ruby. 4 | 5 | Except, where Haskell is overly general and nigh unapproachable, here I try to build an 6 | actually useful set of functional tools for day-to-day Ruby programming 7 | 8 | Features: 9 | 10 | *note, see spec/arrows/proc_spec.rb to get an idea how to use this junk 11 | ### Function composition 12 | 13 | If given 14 | x -> F -> y and y -> G -> z 15 | 16 | Returns 17 | x -> H -> z 18 | 19 | As in we pipe what F poops out into the mouth of G a la Human Centipede 20 | 21 | ```ruby 22 | f = Arrows.lift -> (apple) { apple.to_orange } 23 | g = Arrows.lift -> (orange) { orange.to_kiwi } 24 | apple_to_kiwi = f >> g 25 | ``` 26 | ![Regular >> composition](https://raw.githubusercontent.com/foxnewsnetwork/arrows/master/pics/compose.mermaid.png) 27 | 28 | ### Applicative composition 29 | Calls map (in Haskell, they generalize it to fmap) on the data passed in 30 | 31 | So... If given 32 | x -> F -> y and [x] 33 | 34 | Returns 35 | [x] -> F -> [z] 36 | 37 | ```ruby 38 | apples = Arrows.lift [apple, apple, apple] 39 | apple_to_kiwi = Arrows.lift -> (apple) { apple.to_kiwi } 40 | kiwis = apples >= apple_to_kiwi 41 | ``` 42 | ![functor >= composition](https://raw.githubusercontent.com/foxnewsnetwork/arrows/master/pics/fmap.mermaid.png) 43 | 44 | ### Arrow fan out 45 | x -> y 46 | x -> z 47 | becomes 48 | x -> [y,z] 49 | 50 | ```ruby 51 | apple_to_orange = Arrows.lift -> (apple) { apple.to_orange } 52 | apple_to_kiwi = Arrows.lift -> (apple) { apple.to_kiwi } 53 | orange_and_kiwi = Arrows.lift(apple) >> (apple_to_orange / apple_to_kiwi) 54 | ``` 55 | ![fanout / composition](https://raw.githubusercontent.com/foxnewsnetwork/arrows/master/pics/fanout.mermaid.png) 56 | 57 | ### Arrow Fork 58 | f ^ g produces a proc that takes in a Either, and if either is good, f is evaluated, if either is evil, g is evaluated 59 | 60 | ```ruby 61 | apple_to_orange = Arrows.lift -> (apple) { apple.to_orange } 62 | apple_to_kiwi = Arrows.lift -> (apple) { apple.to_kiwi } 63 | orange = Arrows.lift(apple) >> Arrows::Good >> (apple_to_orange ^ apple_to_kiwi) 64 | kiwi = Arrows.lift(apple) >> Arrows::Evil >> (apple_to_orange ^ apple_to_kiwi) 65 | ``` 66 | ![fork / composition](https://raw.githubusercontent.com/foxnewsnetwork/arrows/master/pics/fork.mermaid.png) 67 | 68 | ### ArrowLoop 69 | step <=> chose produces a proc that cycles between step and chose until chose returns a good either. Then returns whatever was in the good either 70 | 71 | ```ruby 72 | context '<=> feedback' do 73 | let(:step) { Arrows.lift -> (arg_acc) { [arg_acc.first - 1, arg_acc.reduce(&:*)] } } 74 | let(:chose) { Arrows.lift -> (arg_acc) { arg_acc.first < 1 ? Arrows.good(arg_acc.last) : Arrows.evil(arg_acc) } } 75 | let(:factorial) { step <=> chose } 76 | context '120' do 77 | let(:one_twenty) { Arrows.lift(5) >> factorial } 78 | subject { one_twenty.call } 79 | specify { should eq 120 } 80 | end 81 | context '1' do 82 | let(:one) { Arrows.lift(1) >> factorial } 83 | subject { one.call } 84 | specify { should eq 1 } 85 | end 86 | context '0' do 87 | let(:zero) { Arrows.lift(0) >> factorial } 88 | subject { zero.call } 89 | specify { should eq 0 } 90 | end 91 | end 92 | ``` 93 | ![feedback / composition](https://raw.githubusercontent.com/foxnewsnetwork/arrows/master/pics/feedback.mermaid.png) 94 | 95 | ## Memoization 96 | ```ruby 97 | @some_process = Arrows.lift -> (a) { a } 98 | @memoized_process = @some_process.memoize 99 | ``` 100 | 101 | ## Catching errors 102 | ```ruby 103 | @times_two = Arrows.lift -> (x) { x * 2 } 104 | @plus_one = Arrows.lift -> (x) { x == 4 ? raise(SomeError, "error: #{x}") : (x + 1) } 105 | @times_two_plus_one = @times_two >> @plus_one 106 | @caught_process = @times_two_plus_one.rescue_from(SomeError) { |error, arg| "we fucked up on: #{arg}" } 107 | @caught_process.call 1 # 3 108 | @caught_process.call 2 # we fucked up on: 2 109 | ``` 110 | 111 | ## Use Case 112 | Suppose you're running rails (lol what else is there in ruby?) for some sort of ecommerce app and you have an OfferController that handles inputs from an user who is trying to make an offer on some listing you have. Your controller might look like this: 113 | 114 | ```ruby 115 | class OfferController < ApplicationController 116 | rescue_from ActiveRecord::HasCancer 117 | def create 118 | if user_signed_in? 119 | offer = current_user.offers.new offer_params 120 | else 121 | user = User.create! offer_params[:user] 122 | login_in(user) 123 | offer = user.offers.new offer_params 124 | end 125 | if offer.valid? 126 | if offer.save! 127 | mail = OfferNotificationMailer.new_offer offer 128 | mail.deliver! 129 | render offer 130 | end 131 | else 132 | if ...cancer 133 | end 134 | end 135 | private 136 | def stuff 137 | ... 138 | end 139 | 140 | class Offer < ActiveRecord::Base 141 | ... 142 | after_create :queue_some_job, :dispatch_invoice 143 | ... 144 | end 145 | ``` 146 | If it does, I'm sorry to inform you, but your codebase has cancer. Functional Arrows can help resolve this by linearization your currently highly nonlinear Object-Oriented business logic 147 | 148 | ```ruby 149 | class OfferController < ApplicationController 150 | def create 151 | offer_creation_process.call 152 | end 153 | private 154 | def offer_creation_process 155 | offer_params >> initialize_user >> initialize_new_offer >> persist_offer >> deliver_mail / render_view 156 | end 157 | def offer_params 158 | ... 159 | end 160 | def initialize_user 161 | ... 162 | end 163 | ... 164 | end 165 | ``` 166 | That is, offer creation has been reduced back down to a process with distinct steps. 167 | 168 | ### Another Example 169 | Here's an example directly from one of my other applications 170 | ```ruby 171 | class Apiv1::Contacts::UpdateController < Apiv1::UsersController 172 | before_filter :_enforce_correct_user 173 | def update 174 | _update_process.call 175 | end 176 | private 177 | def _enforce_correct_user 178 | unless current_user.admin? || current_user.contacts.include?(_contact) 179 | render json: { message: "This isn't your listing" }, status: 401 180 | end 181 | end 182 | def _update_process 183 | _user_inputs >> _apply_changes >> _decide_validity >> (_update_valid_data_process ^ _render_failure) 184 | end 185 | def _update_valid_data_process 186 | _save_changes >> _decide_primality >> (_make_primary ^ Arrows::ID) >> _render_success 187 | end 188 | def _user_inputs 189 | Arrows.lift _contact 190 | end 191 | def _apply_changes 192 | Arrows.lift -> (contact) { contact.tap { |p| p.assign_attributes _contact_params } } 193 | end 194 | def _decide_validity 195 | Arrows.lift -> (contact) { contact.valid? ? Arrows.good(contact) : Arrows.evil(contact) } 196 | end 197 | def _decide_primality 198 | Arrows.lift -> (contact) { _contact_params[:status] == "primary" ? Arrows.good(contact) : Arrows.evil(contact) } 199 | end 200 | def _render_success 201 | Arrows.lift -> (contact) { render json: { contact: contact.to_ember_hash } } 202 | end 203 | def _save_changes 204 | Arrows.lift -> (contact) { contact.tap(&:save!) } 205 | end 206 | def _make_primary 207 | Arrows.lift -> (contact) { contact.tap &:make_primary! } 208 | end 209 | def _render_failure 210 | Arrows.lift -> (contact) { render json: contact.errors.to_h, status: :expectation_failed } 211 | end 212 | def _contact 213 | @contact ||= Apiv1::UserContact.find params[:id] 214 | end 215 | def _contact_params 216 | @contact_params ||= params.require(:contact).permit(:name, :phone, :email, :address, :status) 217 | end 218 | end 219 | ``` 220 | ## Installation 221 | 222 | Add this line to your application's Gemfile: 223 | 224 | ```ruby 225 | gem 'arrows' 226 | ``` 227 | 228 | And then execute: 229 | 230 | $ bundle 231 | 232 | Or install it yourself as: 233 | 234 | $ gem install arrows 235 | 236 | ## Usage 237 | 238 | TODO: Write usage instructions here 239 | 240 | ## Contributing 241 | 242 | 1. Fork it ( https://github.com/[my-github-username]/arrows/fork ) 243 | 2. Create your feature branch (`git checkout -b my-new-feature`) 244 | 3. Commit your changes (`git commit -am 'Add some feature'`) 245 | 4. Push to the branch (`git push origin my-new-feature`) 246 | 5. Create a new Pull Request 247 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | -------------------------------------------------------------------------------- /arrows.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'arrows/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "arrows" 8 | spec.version = Arrows::VERSION 9 | spec.authors = ["Thomas Chen"] 10 | spec.email = ["foxnewsnetwork@gmail.com"] 11 | spec.summary = %q{Functional programming with composable, applicable, and arrowable functions.} 12 | spec.description = %q{Haskell-like Arrow functionality to procs, includes commonly useful functional junk such as function composition, fanout, functor map composition, parallel composition, fork composition, context lifting, and memoization.} 13 | spec.homepage = "http://github.com/foxnewsnetwork/arrows" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler", "~> 1.7" 22 | spec.add_development_dependency "pry", ">= 0.10" 23 | spec.add_development_dependency "rake", "~> 10.0" 24 | spec.add_development_dependency "rspec", ">=3.1" 25 | end 26 | -------------------------------------------------------------------------------- /lib/arrows.rb: -------------------------------------------------------------------------------- 1 | require "arrows/version" 2 | 3 | module Arrows 4 | require 'arrows/either' 5 | require 'arrows/proc' 6 | class << self 7 | def feedback(merge, split) 8 | Arrows::Proc.new do |args| 9 | single = merge.call [args] 10 | either = split.call single 11 | while not either.good? 12 | single = merge.call either.payload 13 | either = split.call single 14 | end 15 | either.payload 16 | end 17 | end 18 | def good_fork(f) 19 | fork f, ID 20 | end 21 | def evil_fork(g) 22 | fork ID, g 23 | end 24 | def fork(f,g) 25 | Arrows::Proc.new do |either| 26 | either.good? ? f[either.payload] : g[either.payload] 27 | end 28 | end 29 | def concurrent(f,g) 30 | Arrows::Proc.new do |args| 31 | [f[args.first], g[args.last]] 32 | end 33 | end 34 | def fanout(f,g) 35 | Arrows::Proc.new { |args| [f[args], g[args]] } 36 | end 37 | def compose(f,g) 38 | Arrows::Proc.new { |args| g[f[args]] } 39 | end 40 | def fmap(xs, f) 41 | Arrows::Proc.new { |args| xs[args].map { |x| f[x] } } 42 | end 43 | def good(x=nil) 44 | return x if x.respond_to?(:good?) && x.respond_to?(:payload) 45 | Arrows::Either.new true, x 46 | end 47 | def evil(x=nil) 48 | return x if x.respond_to?(:good?) && x.respond_to?(:payload) 49 | Arrows::Either.new false, x 50 | end 51 | def lift(x=nil) 52 | return Arrows::Proc.new { |args| yield args } if block_given? 53 | return x if arrow_like? x 54 | return wrap_proc x if proc_like? x 55 | Arrows::Proc.new { |args| x } 56 | end 57 | def polarize(x=nil) 58 | return lift { |args| yield(args) ? good(args) : evil(args) } if block_given? 59 | return lift { |args| x.call(args) ? good(args) : evil(args) } if proc_like? x 60 | lift { |args| x ? good(args) : evil(args) } 61 | end 62 | def arrow_like?(x) 63 | proc_like?(x) && 64 | x.arity == 1 && 65 | x.respond_to?(:>=) && 66 | x.respond_to?(:>>) && 67 | x.respond_to?(:^) && 68 | x.respond_to?(:/) && 69 | x.respond_to?(:%) 70 | end 71 | def proc_like?(x) 72 | x.respond_to?(:call) && x.respond_to?(:arity) 73 | end 74 | def wrap_proc(f) 75 | Arrows::Proc.new do |args| 76 | f[args] 77 | end 78 | end 79 | end 80 | ID = lift { |x| x } 81 | Good = lift { |x| good x } 82 | Evil = lift { |x| evil x } 83 | Die = lift { |x| throw x } 84 | end 85 | -------------------------------------------------------------------------------- /lib/arrows/either.rb: -------------------------------------------------------------------------------- 1 | class Arrows::Either 2 | attr_accessor :payload 3 | def initialize(good_or_evil, payload) 4 | @good = !!good_or_evil 5 | @payload = payload 6 | end 7 | def good? 8 | @good 9 | end 10 | def evil? 11 | not good? 12 | end 13 | end -------------------------------------------------------------------------------- /lib/arrows/proc.rb: -------------------------------------------------------------------------------- 1 | class Arrows::Proc < Proc 2 | # applicative fmap 3 | def >=(f) 4 | Arrows.fmap self, Arrows.lift(f) 5 | end 6 | 7 | # standard composition 8 | def >>(f) 9 | Arrows.compose self, Arrows.lift(f) 10 | end 11 | 12 | # fanout composition 13 | def /(f) 14 | Arrows.fanout self, Arrows.lift(f) 15 | end 16 | 17 | # concurrent composition 18 | def %(f) 19 | Arrows.concurrent self, Arrows.lift(f) 20 | end 21 | 22 | # fork composition 23 | def ^(f) 24 | Arrows.fork self, f 25 | end 26 | 27 | # feedback (aka ArrowLoop) composition 28 | def <=>(g) 29 | Arrows.feedback self, g 30 | end 31 | 32 | # Returns a memoizing version of this proc 33 | def memoize 34 | cache = {} 35 | Arrows.lift -> (args) { cache.has_key?(args) ? cache[args] : (cache[args] = self[args]) } 36 | end 37 | 38 | # rescues errors from procs 39 | def rescue_from(error_klass=StandardError) 40 | Arrows.lift -> (args) { 41 | begin 42 | self[args] 43 | rescue error_klass => e 44 | yield e, args 45 | end 46 | } 47 | end 48 | end -------------------------------------------------------------------------------- /lib/arrows/version.rb: -------------------------------------------------------------------------------- 1 | module Arrows 2 | VERSION = "0.1.1" 3 | end 4 | -------------------------------------------------------------------------------- /pics/compose.mermaid: -------------------------------------------------------------------------------- 1 | graph LR; 2 | A[apple "->" orange] --> B((compose)) 3 | B --> C[orange "->" kiwi]; -------------------------------------------------------------------------------- /pics/compose.mermaid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxnewsnetwork/arrows/eb51f8b95a20c7485824fb186dd5f77543852929/pics/compose.mermaid.png -------------------------------------------------------------------------------- /pics/concurrent.mermaid: -------------------------------------------------------------------------------- 1 | graph LR; 2 | A[apple, orange] --> X((compose)); 3 | X --> B{concurrent}; 4 | B --> C[apple "->" pear]; 5 | B --> D[orange "->" kiwi]; -------------------------------------------------------------------------------- /pics/concurrent.mermaid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxnewsnetwork/arrows/eb51f8b95a20c7485824fb186dd5f77543852929/pics/concurrent.mermaid.png -------------------------------------------------------------------------------- /pics/fanout.mermaid: -------------------------------------------------------------------------------- 1 | graph LR; 2 | A[apple] --> B((compose)) 3 | B --> C[apple "->" pear]; 4 | B --> D[apple "->" kiwi]; 5 | C --> E{fork}; 6 | D --> E; 7 | E --> F[pear, kiwi]; -------------------------------------------------------------------------------- /pics/fanout.mermaid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxnewsnetwork/arrows/eb51f8b95a20c7485824fb186dd5f77543852929/pics/fanout.mermaid.png -------------------------------------------------------------------------------- /pics/feedback.mermaid: -------------------------------------------------------------------------------- 1 | graph LR; 2 | A[apple] --> B((compose)); 3 | B --> C[apple, mango "->" cherry]; 4 | C --> X((feedback)); 5 | X --> D[cherry "->" Either pear, mango]; 6 | D --> C; 7 | D --> F[pear]; 8 | -------------------------------------------------------------------------------- /pics/feedback.mermaid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxnewsnetwork/arrows/eb51f8b95a20c7485824fb186dd5f77543852929/pics/feedback.mermaid.png -------------------------------------------------------------------------------- /pics/fmap.mermaid: -------------------------------------------------------------------------------- 1 | graph LR; 2 | A[apples] --> B((fmap)); 3 | B --> C[apple "->" pear]; 4 | C --> D[pears]; -------------------------------------------------------------------------------- /pics/fmap.mermaid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxnewsnetwork/arrows/eb51f8b95a20c7485824fb186dd5f77543852929/pics/fmap.mermaid.png -------------------------------------------------------------------------------- /pics/fork.mermaid: -------------------------------------------------------------------------------- 1 | graph LR; 2 | C[apple "->" Either pear, kiwi] --> D{ fork }; 3 | D -->|good| E[pear]; 4 | D -->|evil| F(kiwi); -------------------------------------------------------------------------------- /pics/fork.mermaid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxnewsnetwork/arrows/eb51f8b95a20c7485824fb186dd5f77543852929/pics/fork.mermaid.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxnewsnetwork/arrows/eb51f8b95a20c7485824fb186dd5f77543852929/screenshot.png -------------------------------------------------------------------------------- /spec/arrows/functor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Functor Laws' do 4 | let(:id) do 5 | -> (x) { x } 6 | end 7 | 8 | let(:f) do 9 | -> (x) { ->(y) { x } }.(SecureRandom.base64(1000)).extend(Composable) 10 | end 11 | 12 | let(:g) do 13 | -> (x) { ->(y) { x } }.(SecureRandom.base64(1000)).extend(Composable) 14 | end 15 | 16 | # fmap id = id 17 | describe 'identity' do 18 | context 'when i have []' do 19 | it 'should return []' do 20 | result = Arrows.fmap(Arrows.lift([]), id) 21 | expect(result.()).to eq([]) 22 | end 23 | end 24 | 25 | context 'when i have [:x]' do 26 | it 'should return [:x]' do 27 | result = Arrows.fmap(Arrows.lift([:x]), id) 28 | expect(result.()).to eq([:x]) 29 | end 30 | end 31 | end 32 | 33 | # fmap (f . g) = fmap f . fmap g 34 | describe 'composition' do 35 | context 'when i have []' do 36 | it do 37 | lhs = Arrows.fmap(Arrows.lift([]), f * g) 38 | rhs = Arrows.fmap(Arrows.fmap(Arrows.lift([]), g), f) 39 | expect(lhs.()).to eq(rhs.()) 40 | end 41 | end 42 | 43 | context 'when i have [:x]' do 44 | it do 45 | lhs = Arrows.fmap(Arrows.lift([:x]), f * g) 46 | rhs = Arrows.fmap(Arrows.fmap(Arrows.lift([:x]), g), f) 47 | expect(lhs.()).to eq(rhs.()) 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/arrows/proc_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Arrows::Proc do 4 | context 'ID' do 5 | subject { Arrows::ID } 6 | specify { should be_a Proc } 7 | end 8 | context 'memoize' do 9 | let(:plus_random) { -> (x) { x + rand(9999999999) } } 10 | let(:twelve) { Arrows.lift 4 } 11 | let(:twenty_two) { (twelve >> plus_random).memoize } 12 | subject { twenty_two.call } 13 | specify { should eq twenty_two.call } 14 | end 15 | 16 | context 'polarize' do 17 | let(:numbers) { Arrows.lift [-2,-1,0,1,2,3] } 18 | let(:heaviside) { Arrows.polarize { |x| x > 0 } } 19 | let(:zero) { Arrows.lift { |x| 0 } } 20 | let(:four) { Arrows.lift { |x| 4 } } 21 | let(:zero_or_four) { heaviside >> (zero ^ four) } 22 | let(:actual) { numbers >= zero_or_four } 23 | context "basics" do 24 | subject { heaviside.call 2 } 25 | specify { should be_good } 26 | end 27 | context "full-force" do 28 | subject { actual.call } 29 | specify { should eq [4,4,4,0,0,0] } 30 | end 31 | end 32 | 33 | context 'rescue_from' do 34 | let(:times2 ) { Arrows.lift -> (x) { x * 2 } } 35 | let(:plus1) { Arrows.lift -> (x) { x == 4 ? raise(StandardError, "error: #{x}") : (x + 1) } } 36 | let(:times2_plus1) { times2 >> plus1 } 37 | let(:caught_proc) { times2_plus1.rescue_from { |e, x| "Oh look, we caught:#{x}" } } 38 | let(:two) { Arrows.lift 2 } 39 | let(:five) { two >> caught_proc } 40 | let(:three) { Arrows.lift(1) >> caught_proc } 41 | subject { five.call } 42 | specify { should eq "Oh look, we caught:2" } 43 | context 'regular usage' do 44 | subject { three.call } 45 | specify { should eq 3 } 46 | end 47 | end 48 | 49 | context '<=> feedback' do 50 | let(:step) { Arrows.lift -> (arg_acc) { [arg_acc.first - 1, arg_acc.reduce(&:*)] } } 51 | let(:chose) { Arrows.lift -> (arg_acc) { arg_acc.first < 1 ? Arrows.good(arg_acc.last) : Arrows.evil(arg_acc) } } 52 | let(:factorial) { step <=> chose } 53 | context '120' do 54 | let(:one_twenty) { Arrows.lift(5) >> factorial } 55 | subject { one_twenty.call } 56 | specify { should eq 120 } 57 | end 58 | context '1' do 59 | let(:one) { Arrows.lift(1) >> factorial } 60 | subject { one.call } 61 | specify { should eq 1 } 62 | end 63 | context '0' do 64 | let(:zero) { Arrows.lift(0) >> factorial } 65 | subject { zero.call } 66 | specify { should eq 0 } 67 | end 68 | end 69 | 70 | context '>> composition' do 71 | let(:times2) { -> (x) { x * 2 } } 72 | let(:plus3) { -> (x) { x + 3 } } 73 | let(:four) { Arrows.lift 4 } 74 | let(:fourteen) { four >> plus3 >> times2 } 75 | subject { fourteen.call } 76 | specify { should eq 14 } 77 | end 78 | 79 | context '>= application' do 80 | let(:times2) { -> (x) { x * 2 } } 81 | let(:plus3) { -> (x) { x + 3 } } 82 | let(:twos) { Arrows.lift [2,3,4] } 83 | let(:tens) { twos >= plus3 >= times2 } 84 | subject { tens.call } 85 | specify { should eq [10, 12, 14] } 86 | end 87 | 88 | context '/ fanout' do 89 | let(:times2) { Arrows.lift -> (x) { x * 2 } } 90 | let(:plus3) { Arrows.lift -> (x) { x + 3 } } 91 | let(:four) { Arrows.lift 4 } 92 | let(:six_eight) { four >> times2 / plus3 } 93 | subject { six_eight.call } 94 | specify { should eq [8, 7] } 95 | end 96 | 97 | context '% concurrent' do 98 | let(:times2) { Arrows.lift -> (x) { x * 2 } } 99 | let(:plus3) { Arrows.lift -> (x) { x + 3 } } 100 | let(:four) { Arrows.lift [4,6] } 101 | context 'validity' do 102 | subject { times2.call 2 } 103 | specify { should eq 4 } 104 | end 105 | context 'arity' do 106 | let(:par) { times2 % plus3 } 107 | subject { par.call [1,2] } 108 | specify { should eq [2, 5] } 109 | end 110 | context 'result' do 111 | let(:eight_nine) { four >> times2 % plus3 } 112 | subject { eight_nine.call } 113 | specify { should eq [8,9] } 114 | end 115 | end 116 | 117 | context '^ fork' do 118 | let(:times2) { Arrows.lift -> (x) { x * 2 } } 119 | let(:plus3) { Arrows.lift -> (x) { x + 3 } } 120 | let(:four) { Arrows.lift Arrows.good 4 } 121 | let(:eight) { Arrows.lift Arrows.evil 8 } 122 | let(:fork_four) { four >> (times2 ^ plus3) } 123 | let(:fork_eight) { eight >> (plus3 ^ times2) } 124 | context 'good' do 125 | subject { fork_four.call } 126 | specify { should eq 8 } 127 | end 128 | context 'evil' do 129 | subject { fork_eight.call } 130 | specify { should eq 16 } 131 | end 132 | end 133 | 134 | context '%/ fanout into concurrent' do 135 | let(:add1) { Arrows.lift -> (x) { x + 1 } } 136 | let(:add4) { Arrows.lift -> (x) { x + 4 } } 137 | let(:two) { Arrows.lift 2 } 138 | let(:result) { two >> add1 / add4 >> add1 % add4 } 139 | context 'result' do 140 | subject { result.call } 141 | specify { should eq [4, 10] } 142 | end 143 | end 144 | 145 | context '>=%/ applicative fanout into concurrent' do 146 | let(:add1) { Arrows.lift -> (x) { x + 1 } } 147 | let(:add4) { Arrows.lift -> (x) { x + 4 } } 148 | let(:twos) { Arrows.lift [2,2,2] } 149 | let(:transform) { add1 / add4 >> add1 % add4 } 150 | let(:result) { twos >= add1 / add4 >> add1 % add4 } 151 | context 'result' do 152 | subject { result.call } 153 | specify { should eq [[4,10], [4,10], [4,10]] } 154 | end 155 | context 'similarity' do 156 | let(:actual) { twos >= transform } 157 | subject { result.call } 158 | specify { should eq actual.call } 159 | end 160 | end 161 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # TODO: move the specs in also lol 2 | require 'pry' 3 | require File.expand_path("../../lib/arrows", __FILE__) 4 | 5 | module Composable 6 | def compose(f, g) 7 | -> (x) { f.(g.(x)) } 8 | end 9 | 10 | def *(g) 11 | compose(self, g) 12 | end 13 | end 14 | --------------------------------------------------------------------------------