street_fighter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5a8abf322ddc7f9c321a74ea7697ffa0c0c3840f
4
+ data.tar.gz: a2324b2325d2011fa8bddb87fd0ebc25bb794818
5
+ SHA512:
6
+ metadata.gz: 655589510bd1f2775a15ea1520ae2b98510da2bb43283727fc4f6e94bc81de9cc146165d49c7232ab22a4dc6e1df4ed6d4407dc57ead3dd8531f417b4a739907
7
+ data.tar.gz: 5ea862dca8f4d1c533c4075d56abac0876faf091e50a6730c75070d91be6b11af95e495f980d52e3f7d6c3128bb7583d382424839f827c4b5d7e3b5e08529fe8
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ruby_ukanren.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Stack Builders Inc.
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.
@@ -0,0 +1,105 @@
1
+ # StreetFighter - better error handling for Ruby
2
+
3
+ This library helps to avoid the code smell of "cascading conditionals" (otherwise known as the StreetFighter anti-pattern) by using providing an "Either" data type and interface to your program.
4
+
5
+ ![The StreetFighter anti-pattern as seen in PHP](images/streetfighter.jpg)
6
+
7
+ _Image from [Paul Dragoonis](https://twitter.com/dr4goonis/status/476617165463105536)._
8
+
9
+ ## Example
10
+
11
+ In the game Street Fighter, the hero, Ryu, must defeat 10 opponents. We'll just list three for demonstration:
12
+
13
+ * Retsu (Japan), who has powers including the flying kick and sweep
14
+ * Geki (Japan), who throws ninja stars and is able to teleport
15
+ * Joe (USA), who can do a power kick and low kick
16
+
17
+ _Player info from [Strategy Wiki](http://strategywiki.org/wiki/Street_Fighter/Opponents)._
18
+
19
+ Let's say that Ryu must fight the three opponents above in sequence. He continues battling until he is defeated or he defeats all three opponents. As the game programmer, you must either return a structure representing Ryu, unscathed at the end of the fights, or you must return the opponent who defeated Ryu.
20
+
21
+ A first attempt at modeling this problem may look as follows. We assume there is a function called `battle` that returns the winner of a particular fight. Note the "Street Fighter anti-pattern":
22
+
23
+ ```ruby
24
+ def fight
25
+ if ryu == battle(ryu, retsu)
26
+ if ryu == battle(ryu, geki)
27
+ if ryu == battle(ryu, joe)
28
+ ryu
29
+ else
30
+ joe
31
+ end
32
+ else
33
+ geku
34
+ end
35
+ else
36
+ retsu
37
+ end
38
+ end
39
+ ```
40
+
41
+ It's hard to read, let alone verify that this logic is consistent with the game requirements. Let's try it using the StreetFighter gem. First, we'll create a structure to represent the tournamenters. For now, a simple Struct with a name and a boolean representing whether they're the hero or opponent will suffice:
42
+
43
+ ```ruby
44
+ Player = Struct.new(:name, :hero)
45
+ ```
46
+
47
+ Let's define the hero and three opponents:
48
+
49
+ ```ruby
50
+ ryu = Player.new(:ryu, true) # The hero!
51
+
52
+ # The bad guys.
53
+ retsu = Player.new(:retsu, false)
54
+ geki = Player.new(:geki, false)
55
+ joe = Player.new(:joe, false)
56
+
57
+ ```
58
+
59
+ And the fighting function:
60
+
61
+ ```ruby
62
+ # Perform a random battle, giving the hero 3 chances to win to every
63
+ # 1 chance for the opponent. We must return the winner wrapped in a `Left`
64
+ # if the winner is the opponent, or a `Right` if the winner is our hero.
65
+ def battle(opponent, hero)
66
+ winner = ([hero] * 3 << opponent).sample
67
+ winner.hero ? StreetFighter::Right.new(winner) :
68
+ StreetFighter::Left.new(winner)
69
+ end
70
+ ```
71
+
72
+ Finally we define the rounds of the game. We're going to use [currying](http://en.wikipedia.org/wiki/Currying) here
73
+ so that we can apply an opponent to each battle, and wind up with a partially-applied function. Essentially, each partially-applied function can be thought of as an opponent angrily waiting until he has the opportunity to try to do serious damage to our hero, Ryu.
74
+
75
+ ```ruby
76
+ fight = method(:battle).to_proc.curry
77
+ ```
78
+
79
+ I hope you're ready - the fight is about to begin!
80
+
81
+ ```ruby
82
+ winner = StreetFighter.tournament(ryu, fight[retsu], fight[geki], fight[joe])
83
+ ```
84
+
85
+ All that's left is to see the results. Having the result of the computation wrapped in an `EitherValue` (`Right` or `Left`) facilitates using a simple case statement on the return value:
86
+
87
+ ```ruby
88
+ case winner
89
+ when StreetFighter::Left
90
+ puts "Our hero has been defeated, and #{winner.value.name} is the new champion."
91
+ when StreetFighter::Right
92
+ puts "Ryu has defeated his opponents!"
93
+ end
94
+ ```
95
+
96
+ Fortunately, the output is:
97
+
98
+ Ryu has defeated his opponents!
99
+
100
+ Not only has Ryu won this round, but we've completely defeated the cascading conditionals by applying some easy-to-use functional patterns. If you'd like to tournament around with the game and see if Ryu keeps up his winning streak, [it's a part of the test suite for this library](spec/street_fighter/street_fighter_game_spec.rb).
101
+
102
+ ## Credits
103
+
104
+ Thanks to [Paul Dragoonis](https://twitter.com/dr4goonis) for [identifying
105
+ this anti-pattern and giving us a great graphical illustration of the beast in the wild](https://twitter.com/dr4goonis/status/476617165463105536).
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << "spec"
7
+ t.test_files = FileList['spec/**/*_spec.rb']
8
+ end
9
+
10
+ task :default => :test
Binary file
@@ -0,0 +1,13 @@
1
+ require 'street_fighter/version'
2
+
3
+ require 'street_fighter/either_value'
4
+ require 'street_fighter/right'
5
+ require 'street_fighter/left'
6
+ require 'street_fighter/either'
7
+ require 'street_fighter/either_check'
8
+
9
+ module StreetFighter
10
+ def self.tournament(hero, *opponents)
11
+ Right.new(hero).tournament(*opponents)
12
+ end
13
+ end
@@ -0,0 +1,10 @@
1
+ module StreetFighter
2
+ class Either
3
+ attr_reader :left, :right
4
+
5
+ def initialize left_value, right_value
6
+ @left = Left.new(left_value)
7
+ @right = Right.new(right_value)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,6 @@
1
+ module StreetFighter
2
+ class EitherCheck < Struct.new(:klass)
3
+ def run! ; raise ArgumentError unless klass.is_a?(EitherValue) ; end
4
+ end
5
+ end
6
+
@@ -0,0 +1,25 @@
1
+ module StreetFighter
2
+ class EitherValue
3
+ attr_reader :value
4
+
5
+ def initialize value
6
+ @value = value
7
+ end
8
+
9
+ def match # (>>) in Haskell
10
+ raise NotImplementedError, "Follows not implemented here."
11
+ end
12
+
13
+ def bind # (>>=) in Haskell
14
+ raise NotImplementedError, "Bind not implemented here."
15
+ end
16
+
17
+ def failable # `fmap` in Haskell, but restricted to EitherValues
18
+ raise NotImplementedError, "Bind not implemented here."
19
+ end
20
+
21
+ def ==(other)
22
+ self.value == other.value
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,17 @@
1
+ module StreetFighter
2
+ class Left < EitherValue
3
+ def match other # >> (then)
4
+ EitherCheck.new(other).run!
5
+ self
6
+ end
7
+
8
+ def tournament *fns
9
+ self
10
+ end
11
+
12
+ def bind func
13
+ self
14
+ end
15
+ end
16
+ end
17
+
@@ -0,0 +1,20 @@
1
+ module StreetFighter
2
+ class Right < EitherValue
3
+ def match other
4
+ EitherCheck.new(other).run!
5
+ other
6
+ end
7
+
8
+ def tournament *fns
9
+ return self if fns.empty?
10
+
11
+ bind(fns.first).tap do |result|
12
+ EitherCheck.new(result).run!
13
+ end.tournament(*fns[1..-1])
14
+ end
15
+
16
+ def bind func
17
+ func.call(value)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ module StreetFighter
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,5 @@
1
+ require 'minitest/autorun'
2
+
3
+ require "#{File.dirname(__FILE__)}/../lib/street_fighter"
4
+
5
+ include StreetFighter
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+
3
+ module StreetFighter
4
+ describe "#match" do
5
+ it "returns the first Left after a sequence of Right values" do
6
+ Left.new("first").match(Right.new("second")).
7
+ must_equal Left.new("first")
8
+ end
9
+
10
+ it "returns a Right if all values in the sequence are Right" do
11
+ Right.new("first").match(Right.new("second")).
12
+ must_equal Right.new("second")
13
+ end
14
+ end
15
+
16
+ describe "binding and mapping over failable tests" do
17
+
18
+ # The basic data structure we'll be testing is a Person.
19
+ Person = Struct.new(:name, :age)
20
+
21
+ # A helper method that returns a value wrapped in an Either based on the
22
+ # boolean test.
23
+ def failable_test person, bool, msg
24
+ bool ? Right.new(person) : Left.new(msg)
25
+ end
26
+
27
+ def bob? person
28
+ failable_test person, person.name == 'Bob', 'The name should have been Bob!'
29
+ end
30
+
31
+ def old_enough? person
32
+ failable_test person, person.age >= 21, 'Person is not old enough!'
33
+ end
34
+
35
+ describe "#bind" do
36
+ it "returns the Right value if the function is successful" do
37
+ bob = Person.new("Bob", 22)
38
+ valid_age = method(:old_enough?)
39
+
40
+ bob?(bob).bind(valid_age).must_equal(Right.new(bob))
41
+ end
42
+
43
+ it "returns the first Left value encountered in a sequence" do
44
+ bob = Person.new("Tom", 22)
45
+ valid_age = method(:old_enough?)
46
+
47
+ bob?(bob).bind(valid_age).
48
+ must_equal(Left.new("The name should have been Bob!"))
49
+ end
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+
56
+
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ describe "tournamenting a game of street fighter" do
4
+
5
+ Player = Struct.new(:name, :hero)
6
+
7
+ # Perform a random battle, giving the hero 3 chances to win to every
8
+ # 1 chance for the opponent. We must return the winner wrapped in a `Left`
9
+ # if the winner is the opponent, or a `Right` if the winner is our hero.
10
+ def battle(opponent, hero)
11
+ winner = ([hero] * 3 << opponent).sample
12
+ winner.hero ? StreetFighter::Right.new(winner) :
13
+ StreetFighter::Left.new(winner)
14
+ end
15
+
16
+
17
+ it "always returns a Left or a Right value" do
18
+ ryu = Player.new(:ryu, true) # The hero!
19
+
20
+ # The bad guys.
21
+ retsu = Player.new(:retsu, false)
22
+ geki = Player.new(:geki, false)
23
+ joe = Player.new(:joe, false)
24
+
25
+ fight = method(:battle).to_proc.curry
26
+
27
+ winner = StreetFighter.tournament(ryu, fight[retsu], fight[geki], fight[joe])
28
+
29
+ # case winner
30
+ # when StreetFighter::Left
31
+ # puts "Our hero has been defeated, and #{winner.value.name} is the new champion."
32
+ # when StreetFighter::Right
33
+ # puts "Ryu has defeated his opponents!"
34
+ # end
35
+
36
+ winner.must_be_kind_of StreetFighter::EitherValue
37
+ end
38
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ describe StreetFighter do
4
+ it "returns the final Right value if sequence only results in Rights" do
5
+ fight1 = Proc.new{|_| Right.new("hey") }
6
+ fight2 = Proc.new{|_| Right.new("hey") }
7
+
8
+ StreetFighter.tournament(Object.new, fight1, fight2).
9
+ must_equal Right.new("hey")
10
+ end
11
+
12
+ it "returns the first Left value if a computation fails in the sequence" do
13
+ fight1 = Proc.new{|_| Right.new("hey") }
14
+ fight2 = Proc.new{|_| Left.new("fail!") }
15
+
16
+ StreetFighter.tournament(Object.new, fight1, fight2).must_equal Left.new("fail!")
17
+ end
18
+
19
+ it "raises an ArgumentError if the function doesn't return an EitherValue" do
20
+ proc { StreetFighter.tournament(Object.new, Proc.new{ Object.new }) }.
21
+ must_raise ArgumentError
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'street_fighter/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "street_fighter"
8
+ spec.version = StreetFighter::VERSION
9
+ spec.authors = ["Justin Leitgeb"]
10
+ spec.email = ["support@stackbuilders.com"]
11
+ spec.summary = %q{An implementation of an Either monad in Ruby}
12
+ spec.description = %q{Implements an Either monad in Ruby for cleaner error handling.}
13
+ spec.homepage = "http://github.com/stackbuilders/street_fighter"
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.5"
22
+ spec.add_development_dependency "rake", "~> 10"
23
+ end
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: street_fighter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Justin Leitgeb
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.5'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10'
41
+ description: Implements an Either monad in Ruby for cleaner error handling.
42
+ email:
43
+ - support@stackbuilders.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".gitignore"
49
+ - Gemfile
50
+ - LICENSE.txt
51
+ - README.md
52
+ - Rakefile
53
+ - images/streetfighter.jpg
54
+ - lib/street_fighter.rb
55
+ - lib/street_fighter/either.rb
56
+ - lib/street_fighter/either_check.rb
57
+ - lib/street_fighter/either_value.rb
58
+ - lib/street_fighter/left.rb
59
+ - lib/street_fighter/right.rb
60
+ - lib/street_fighter/version.rb
61
+ - spec/spec_helper.rb
62
+ - spec/street_fighter/failable_spec.rb
63
+ - spec/street_fighter/street_fighter_game_spec.rb
64
+ - spec/street_fighter_spec.rb
65
+ - street_fighter.gemspec
66
+ homepage: http://github.com/stackbuilders/street_fighter
67
+ licenses:
68
+ - MIT
69
+ metadata: {}
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubyforge_project:
86
+ rubygems_version: 2.2.2
87
+ signing_key:
88
+ specification_version: 4
89
+ summary: An implementation of an Either monad in Ruby
90
+ test_files:
91
+ - spec/spec_helper.rb
92
+ - spec/street_fighter/failable_spec.rb
93
+ - spec/street_fighter/street_fighter_game_spec.rb
94
+ - spec/street_fighter_spec.rb
95
+ has_rdoc: