factree 0.5.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/CODEOWNERS +1 -0
  3. data/README.md +193 -2
  4. data/lib/factree/version.rb +1 -1
  5. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 85ee803b7b9d0ab2cbc2976de3003ebecf2298f7
4
- data.tar.gz: 69fb2de29736f1b6c8ef2bd69103443f0c01a169
3
+ metadata.gz: 8a171e984d21c6824ec69bb8a461563f96cbaf95
4
+ data.tar.gz: 25e9413763b51a2a77aa217666d85853f9035ba9
5
5
  SHA512:
6
- metadata.gz: 97c439bb519778fdadfb8122963dd7ead6550714f41070726e0d983b21bd76b0a96218af9489008b7f93266ec4042042478124c37c0a70e6794022f92a622c90
7
- data.tar.gz: 3b37ad1ce717ca70a19d380e8a36172c0e2af65f4e195cbe8b920b0e6d083a3083031fea5a5b2b7d0c07c601a07d6123e4b1c31f3a4a42af3a8e7d91d7d3a582
6
+ metadata.gz: 9b1c0dbde274679313374c443e1bc8789a41ab77f89c62d03ea5b099fe544715889e7b68e54bdb71860930c68b50a76cb7d7137427f8f790658653b40d19a75c
7
+ data.tar.gz: 632b3d41a120579b617bba30f8afbc1283dd33d67c871fe5ae2efde350bbf613f35a599ed082cde1c5d55b25854bd333dab06900f5aff26a642c8b0235d6963d
@@ -0,0 +1 @@
1
+ * @jstrater
data/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # Factree
2
2
  [![Gem Version](https://badge.fury.io/rb/factree.svg)](https://rubygems.org/gems/factree)
3
3
  [![Build Status](https://travis-ci.org/ConsultingMD/factree.svg?branch=master)](https://travis-ci.org/ConsultingMD/factree)
4
+ [![YARD Docs](https://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/gems/factree)
4
5
 
5
- 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 it doesn't, then Factree will tell you what's needed to continue.
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
 
@@ -1,3 +1,3 @@
1
1
  module Factree
2
- VERSION = "0.5.0"
2
+ VERSION = "0.5.1"
3
3
  end
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.0
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-07-06 00:00:00.000000000 Z
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