street_fighter 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +105 -0
- data/Rakefile +10 -0
- data/images/streetfighter.jpg +0 -0
- data/lib/street_fighter.rb +13 -0
- data/lib/street_fighter/either.rb +10 -0
- data/lib/street_fighter/either_check.rb +6 -0
- data/lib/street_fighter/either_value.rb +25 -0
- data/lib/street_fighter/left.rb +17 -0
- data/lib/street_fighter/right.rb +20 -0
- data/lib/street_fighter/version.rb +3 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/street_fighter/failable_spec.rb +56 -0
- data/spec/street_fighter/street_fighter_game_spec.rb +38 -0
- data/spec/street_fighter_spec.rb +23 -0
- data/street_fighter.gemspec +23 -0
- metadata +95 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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).
|
data/Rakefile
ADDED
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,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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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:
|