factree 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CODEOWNERS +1 -0
- data/README.md +193 -2
- data/lib/factree/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8a171e984d21c6824ec69bb8a461563f96cbaf95
|
4
|
+
data.tar.gz: 25e9413763b51a2a77aa217666d85853f9035ba9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9b1c0dbde274679313374c443e1bc8789a41ab77f89c62d03ea5b099fe544715889e7b68e54bdb71860930c68b50a76cb7d7137427f8f790658653b40d19a75c
|
7
|
+
data.tar.gz: 632b3d41a120579b617bba30f8afbc1283dd33d67c871fe5ae2efde350bbf613f35a599ed082cde1c5d55b25854bd333dab06900f5aff26a642c8b0235d6963d
|
data/CODEOWNERS
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
* @jstrater
|
data/README.md
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
# Factree
|
2
2
|
[](https://rubygems.org/gems/factree)
|
3
3
|
[](https://travis-ci.org/ConsultingMD/factree)
|
4
|
+
[](http://www.rubydoc.info/gems/factree)
|
4
5
|
|
5
|
-
|
6
|
+
Have a complicated decision to make? Factree will guide you through it step by step, identifying exactly which questions you need to answer along the way in order to reach a conclusion.
|
6
7
|
|
7
8
|
## Installation
|
8
9
|
|
@@ -22,7 +23,197 @@ Or install it yourself as:
|
|
22
23
|
|
23
24
|
## Usage
|
24
25
|
|
25
|
-
[API documentation](http://www.rubydoc.info/gems/factree)
|
26
|
+
*For details, check out the [API documentation](http://www.rubydoc.info/gems/factree).*
|
27
|
+
|
28
|
+
### Finding paths through a decision function
|
29
|
+
|
30
|
+
Factree provides tools for making choices based on a set of facts that are not yet known. You write a decision function that takes a set facts and returns a conclusion. Factree will run your function and make sure it has all of the facts it needs to complete. If any are missing, Factree will tell you what's needed to continue.
|
31
|
+
|
32
|
+
For example, say I want to pick an animal based on its attributes. First I'll write a function to make the decision.
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
include Factree::DSL
|
36
|
+
|
37
|
+
decide = ->(facts) do
|
38
|
+
return conclusion :turtle unless facts[:mammal?]
|
39
|
+
|
40
|
+
if facts[:herbivore?]
|
41
|
+
conclusion :rabbit
|
42
|
+
else
|
43
|
+
conclusion :dog
|
44
|
+
end
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
Then I'll pass that function to {Factree.find_path `find_path`} without any facts.
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
path = find_path &decide
|
52
|
+
|
53
|
+
path.complete?
|
54
|
+
#=> false
|
55
|
+
|
56
|
+
path.required_facts
|
57
|
+
#=> [:mammal?]
|
58
|
+
```
|
59
|
+
|
60
|
+
`find_path` will run my `decide` function until it reaches a conclusion or asks for a fact that is unknown. In this case, my function checks `facts[:mammal?]` right off the bat, and since I didn't provide that fact, `find_path` stopped there. The path through my decision tree was incomplete.
|
61
|
+
|
62
|
+
Thankfully, `find_path` keeps track of all of the facts that are requested as it makes its way through a decision function. If it has to stop because a fact is unknown, the fact's name will be included in {Path.required_facts `required_facts`}. You can check `required_facts` to see exactly what's required to progress through the function.
|
63
|
+
|
64
|
+
Let's give `find_path` another try, this time with `mammal?: false`.
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
path = find_path mammal?: false, &decide
|
68
|
+
|
69
|
+
path.complete?
|
70
|
+
#=> true
|
71
|
+
|
72
|
+
path.conclusion
|
73
|
+
#=> :turtle
|
74
|
+
```
|
75
|
+
|
76
|
+
This time `find_path` had all of the facts it needed to reach a conclusion, so it returned a complete path.
|
77
|
+
|
78
|
+
Supplying different values for facts may lead to a different path through the decision function, changing the facts that are required. For example, if `mammal?` is true, then we'll also need `herbivore?` to get to a conclusion.
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
path = find_path mammal?: true, &decide
|
82
|
+
|
83
|
+
path.complete?
|
84
|
+
#=> false
|
85
|
+
|
86
|
+
path.required_facts
|
87
|
+
#=> [:mammal?, :herbivore?]
|
88
|
+
```
|
89
|
+
|
90
|
+
### Using `Factree::DSL`
|
91
|
+
|
92
|
+
Including {Factree::DSL `Factree::DSL`} in your code isn't mandatory. It just makes certain methods (like `find_path` and `conclusion`) easier to access. You can call the same methods on the {Factree `Factree`} module.
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
Factree.find_path **facts, &decide
|
96
|
+
|
97
|
+
# is the same as
|
98
|
+
|
99
|
+
include Factree::DSL
|
100
|
+
find_path **facts, &decide
|
101
|
+
```
|
102
|
+
|
103
|
+
### Supplying facts
|
104
|
+
|
105
|
+
Factree is designed to work with decision functions that depend on many different facts. The {Factree::FactSource FactSource} mixin can help you organize them. A fact source class also provides a place for you to distill complex data into simple values, allowing you to keep your decision functions concise and readable.
|
106
|
+
|
107
|
+
Here's an example of a fact source that supplies a set of facts to help select a car for a buyer.
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
class DriverFactSource
|
111
|
+
include Factree::FactSource
|
112
|
+
|
113
|
+
def initialize(person)
|
114
|
+
@person = person
|
115
|
+
freeze
|
116
|
+
end
|
117
|
+
|
118
|
+
def_fact(:needs_fast_car?) { @person.name == 'Ricky Bobby' }
|
119
|
+
|
120
|
+
def_fact(:needs_cheap_car?) { @person.account_balance < 500.00 }
|
121
|
+
|
122
|
+
def_fact(:needs_extra_seats?) do
|
123
|
+
unknown if @person.family_size == :unknown
|
124
|
+
|
125
|
+
@person.family_size > 4
|
126
|
+
end
|
127
|
+
end
|
128
|
+
```
|
129
|
+
|
130
|
+
Our DriverFactSource is just a class with some method-like fact definitions. Let's instantiate it for a person and see how it works.
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
Person = Struct.new(:name, :account_balance, :family_size)
|
134
|
+
tom = Person.new("Tom", 10_000.00, :unknown)
|
135
|
+
source = DriverFactSource.new(tom)
|
136
|
+
```
|
137
|
+
|
138
|
+
The source's primary job is to crank out a set of facts that can be fed into `find_path`.
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
source.to_h
|
142
|
+
#=> {:needs_fast_car?=>false, :needs_cheap_car?=>false}
|
143
|
+
```
|
144
|
+
|
145
|
+
Two of the facts we defined are included, but you might have noticed that `:needs_extra_seats?` is missing. Since we're dealing with facts that might not be known, FactSource provides an easy way to conditionally omit those facts. Just call {Factree::FactSource#unknown `#unknown`} instead of returning a value, and the fact will be omitted from the collection returned by {Factree::FactSource#to_h `#to_h`}. Attempting to {Factree::FactSource#fetch `#fetch`} it will raise an error.
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
source.fetch(:needs_extra_seats?)
|
149
|
+
# Factree::FactSource::UnknownFactError: unknown fact: needs_extra_seats?
|
150
|
+
```
|
151
|
+
|
152
|
+
### Organizing your decision functions
|
153
|
+
|
154
|
+
It's recommended that you define your decision functions in their own modules, particularly when they're large or complex. For example:
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
module Decisions
|
158
|
+
def self.decide_something(facts)
|
159
|
+
# ...
|
160
|
+
end
|
161
|
+
end
|
162
|
+
```
|
163
|
+
|
164
|
+
A method reference can be used to pass your decision function to `#find_path`.
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
find_path **facts, &Decisions.method(:decide_something)
|
168
|
+
```
|
169
|
+
|
170
|
+
#### Splitting up large functions
|
171
|
+
|
172
|
+
Factree comes with a tool for splitting big decisions into separate functions to simplify them and make them independently testable. Take this cheese choice, for example.
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
def choose_cheese(facts)
|
176
|
+
if facts[:soft?]
|
177
|
+
return conclusion :brie unless facts[:blue?]
|
178
|
+
return conclusion :gorgonzola if facts[:cows_milk?]
|
179
|
+
conclusion :brie
|
180
|
+
else
|
181
|
+
return conclusion :pecorino_toscano unless facts[:from_cows_milk?]
|
182
|
+
return conclusion :emmental if facts[:holes?]
|
183
|
+
conclusion :gruyere
|
184
|
+
end
|
185
|
+
end
|
186
|
+
```
|
187
|
+
|
188
|
+
Say I wanted to split up my choice into separate functions for soft and hard cheeses. I can use {Factree.decide_between_alternatives `decide_between_alternatives`} to accomplish that.
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
def choose_soft_cheese(facts)
|
192
|
+
return unless facts[:soft?]
|
193
|
+
|
194
|
+
return conclusion :brie unless facts[:blue?]
|
195
|
+
return conclusion :gorgonzola if facts[:cows_milk?]
|
196
|
+
conclusion :roquefort
|
197
|
+
end
|
198
|
+
|
199
|
+
def choose_hard_cheese(facts)
|
200
|
+
return if facts[:soft?]
|
201
|
+
|
202
|
+
return conclusion :pecorino_toscano unless facts[:from_cows_milk?]
|
203
|
+
return conclusion :emmental if facts[:holes?]
|
204
|
+
conclusion :gruyere
|
205
|
+
end
|
206
|
+
|
207
|
+
def choose_cheese(facts)
|
208
|
+
decide_between_alternatives facts,
|
209
|
+
method(:choose_soft_cheese),
|
210
|
+
method(:choose_hard_cheese)
|
211
|
+
end
|
212
|
+
```
|
213
|
+
|
214
|
+
`decide_between_alternatives` will try each decision function in order. If the first returns `nil`, it will try the second, and so on, until a conclusion or an unknown fact is reached.
|
215
|
+
|
216
|
+
By splitting up my decision method into two separate ones, I've made them independently testable. They can also be reordered, and I've eliminated a level of nesting inside of a conditional statement.
|
26
217
|
|
27
218
|
## Development
|
28
219
|
|
data/lib/factree/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: factree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.
|
4
|
+
version: 0.5.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josh Strater
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-08-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -91,6 +91,7 @@ files:
|
|
91
91
|
- ".ruby-version"
|
92
92
|
- ".travis.yml"
|
93
93
|
- ".yardopts"
|
94
|
+
- CODEOWNERS
|
94
95
|
- Gemfile
|
95
96
|
- LICENSE.txt
|
96
97
|
- README.md
|