speculation 0.4.0 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7aa989a5796d79c562dc7ade44fe3b9b146b208e
4
- data.tar.gz: 4e3249fae769e64f274f040ad59ef012ed4e50f5
3
+ metadata.gz: 8931f31e4324a18fab4cbc70ba363c072a0fcf78
4
+ data.tar.gz: 886f8a36b120d8dcebf90a139ec9a0d74817759d
5
5
  SHA512:
6
- metadata.gz: bf7cb77e9f081fa6605a7676fd733c1ea5a2d3d8da9634bf529bdfd523e37cf54795e96dabdc84e039c920efbd2e1a620f7fcd3d7abbff16c5d8eed90ca0a28d
7
- data.tar.gz: b997fa5b070e09ac6c146bb0587278351e03da7bb27ef12d650765484c66045e7f05576bbdea13ccfaf8f4b72e44d53e1ab54e52149eafff7e3b10f48ee78d54
6
+ metadata.gz: c1cfef5d20928e45e5852a6e25ea04e28fdce96aa36dc3f877e364ba63af52018983bfdfbee523b0221fd29a160a1a0c2dd45c33c3ebe372531fff1e9e6b2dfa
7
+ data.tar.gz: bb4eb7ded2d8ab2335d9993b2185492d0821e59c65d362b6152af6d06d551867b39dcf6eea23a98eb3e25cbb7dd9d2b3e81485db90b5ff0f46c75a939f25e2d5
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A Ruby port of Clojure's `clojure.spec`. This library is largely a copy-and-paste from clojure.spec so all credit goes to Rich Hickey and contributors for their original work.
4
4
 
5
- See [clojure.spec - Rationale and Overview](https://clojure.org/about/spec) for a thorough outline of the problems clojure.spec (and thus Speculation) was built to address and how it addresses them. As a brief summary, Speculation allows you to write predicate specs (nothing to do with RSpec!) which enable:
5
+ See [clojure.spec - Rationale and Overview](https://clojure.org/about/spec) for a thorough outline of the problems clojure.spec (and thus Speculation) was built to address and how it addresses them. As a brief summary, Speculation allows you to describe the structure of datastructures and methods, enabling:
6
6
 
7
7
  * declarative data validation and destructuring
8
8
  * error reporting
@@ -11,85 +11,178 @@ See [clojure.spec - Rationale and Overview](https://clojure.org/about/spec) for
11
11
 
12
12
  ## Project Goals
13
13
 
14
- The goal of this project is to match clojure.spec as closely as possible, from design to features to API. There aren't, and won't be any, significant departures from clojure.spec.
14
+ The goal of this project is to match clojure.spec as closely as possible, from design to features to API. There aren't, and won't be, any significant departures from clojure.spec.
15
15
 
16
- ## Examples
16
+ ## Usage
17
17
 
18
- * [sinatra-web-app](examples/sinatra-web-app): A small Sinatra web application demonstrating parameter validation and API error message generation.
19
- * [spec_guide.rb](examples/spec_guide.rb): Speculation port of Clojure's [spec guide](https://clojure.org/guides/spec), demonstrating most features.
20
- * [codebreaker.rb](examples/codebreaker.rb): Speculation port of the 'codebreaker' game described in [Interactive development with clojure.spec](http://blog.cognitect.com/blog/2016/10/5/interactive-development-with-clojurespec)
21
- * [json_parser.rb](examples/json_parser.rb): JSON parser using Speculation.
18
+ API Documentation is available at [RubyDoc](http://www.rubydoc.info/github/english/speculation) and [the wiki](https://github.com/english/speculation/wiki) covers features at a higher level. The API is more-or-less the same as `clojure.spec` so if you're already familiar clojure.spec with then you should feel at home with Speculation. Most guides, talks and discussions around clojure.spec should apply equally well to Speculation.
22
19
 
23
- ## Usage
20
+ To demonstrate most of the features of Speculation we can explore an implementation of [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life):
21
+
22
+ ### Game of Life demonstration
23
+
24
+ First, we'll require the [speculation][speculation] library along with the [generator][gen] and [test][test] modules. We'll be referring to `Speculation` a lot, so we'll add a shorthand to save us some typing:
24
25
 
25
- Documentation is available at [RubyDoc](http://www.rubydoc.info/github/english/speculation). The API is more-or-less the same as `clojure.spec`. If you're already familiar clojure.spec with then you should feel at home with Speculation. Most guides, talks and discussion around clojure.spec should apply equally well to Speculation. Clojure and Ruby and quite different languages, so naturally there are some differences:
26
+ ```ruby
27
+ require 'speculation'
28
+ require 'speculation/test'
29
+ require 'speculation/gen'
26
30
 
27
- ## Differences with clojure.spec
31
+ S = Speculation
32
+ ```
33
+
34
+ The Game of Life can be modelled with just a few simple entities. Let's describe them up front:
28
35
 
29
- ### Built in predicates
36
+ ```ruby
37
+ # Our 'world' is a set of 'cells'.
38
+ S.def :"gol/world", S.coll_of(:"gol/cell", kind: Set)
30
39
 
31
- clojure.spec utilises its rich standard library of predicate functions and data structures when writing specs. Ruby has neither of those, so we must be creative with what we define as a 'predicate' in Speculation. Each of the following are valid Speculation predicates:
40
+ # A cell is a tuple of coordinates.
41
+ S.def :"gol/cell", S.tuple(:"gol/coordinate", :"gol/coordinate")
32
42
 
33
- ```rb
34
- S.valid?(->(x) { x > 0 }, 2)
35
- S.valid?(:even?.to_proc, 2)
36
- S.valid?(String, "foo")
37
- S.valid?(Enumerable, [1, 2, 3])
38
- S.valid?(/^\d+$/, "123")
39
- S.valid?(Set[:foo, :bar, :baz], :foo)
43
+ # A coordinate is just an integer.
44
+ S.def :"gol/coordinate", S.with_gen(Integer) { S.gen(S.int_in(-5..5)) }
40
45
  ```
41
46
 
42
- ### Namespaced keywords/symbols
47
+ Let's unpick what we've done so far:
43
48
 
44
- Namespaced keywords are at the core of `clojure.spec`. Since clojure.spec utilises a global spec registry, namespaced keywords allow libraries to register specs with the same names but under different namespaces, thus removing accidental collisions. Ruby's equivalent to Clojure's keywords are Symbols. Ruby Symbol's don't have namespaces.
49
+ - We've registered 'specs' (via [`S.def`][s-def]) to a global registry of specs, naming then with [namespaced Symbols][ns-symbols].
50
+ - We've created both complex ([`S.coll_of`][coll_of] and [`S.tuple`][tuple]) and simple (`Integer`) specs.
51
+ - We've also leveraged the built-in generators for `S.coll_of` and `S.tuple` but swapped out the `Integer` `:"gol/coordinate"` generator for another built-in: [`S.int_in`][int_in]. While we don't have an upper or lower bound on a valid coordinate, using a more restrictive generator allows us to experiment with smaller worlds.
45
52
 
46
- In order keep the global spec registry architecture in Speculation, we utilise a helper method `ns` to achieve similar behaviour:
53
+ Before we move on to implementing the logic of the Game of Life, let's get an idea of the kind of data we'll be working with.
54
+
55
+ ```ruby
56
+ S::Gen.generate(S.gen(:"gol/world"))
57
+ # => #<Set: {[2, -3], [-3, 5], [1, 1], [0, -3], [-5, 2], [-2, -3]}>
58
+ S::Gen.generate(S.gen(:"gol/world"))
59
+ # => #<Set: {}>
60
+ ```
47
61
 
48
- ```rb
49
- module MyModule
50
- extend Speculation::NamespacedSymbols
62
+ Our up front definition of specs is already paying off. We can generate random, valid examples of our expected domain entities and play around with them, either in tests or in a REPL (e.g. Pry, IRB). Without this kind of exploration we may not have initially considered the case where the world is empty!
51
63
 
52
- p ns(:foo)
53
- # => :"MyModule/foo"
64
+ Now to the logic of the game. Before we can implement [the rules](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life#Rules), we must be able to find the neighbouring cells for a given cell.
54
65
 
55
- p ns(AnotherModule, :foo)
56
- # => :"AnotherModule/foo"
66
+ ```ruby
67
+ def self.neighbours(cell)
68
+ cell_x, cell_y = cell
69
+ (-1..1).to_a.repeated_permutation(2).map { |(x, y)| [cell_x - x, cell_y - y] }
57
70
  end
58
71
  ```
59
72
 
60
- ### FSpecs
73
+ Now I think that should work... But I'm not so confident. Let's write a spec for the method. Its argument should be a cell, which we already have a spec for. We'll assume our world has no boundaries, so any cell should have a set of 8 neighbours.
61
74
 
62
- #### Symbols/Methods
75
+ ```ruby
76
+ S.fdef method(:neighbours),
77
+ args: S.cat(cell: :"gol/cell"),
78
+ ret: S.coll_of(:"gol/cell", count: 8, kind: Set)
79
+ ```
80
+
81
+ Now that we've described the inputs and outputs of the method, we can generatively test it:
82
+
83
+ ```ruby
84
+ S::Test.summarize_results S::Test.check(method(:neighbours))
85
+ # {:spec=>"Speculation::FSpec(main.neighbours)",
86
+ # :method=>#<Method: main.neighbours>,
87
+ # :failure=>
88
+ # {:problems=>
89
+ # [{:path=>[:ret],
90
+ # :val=>[[-1, 6], [-1, 5], [-1, 4], [-2, 6], [-2, 5], [-2, 4], [-3, 6], [-3, 5], [-3, 4]],
91
+ # :via=>[],
92
+ # :in=>[],
93
+ # :pred=>[Set,[[[-1, 6], [-1, 5], [-1, 4], [-2, 6], [-2, 5], [-2, 4], [-3, 6], [-3, 5], [-3, 4]]]]}],
94
+ # :spec=>Speculation::EverySpec(),
95
+ # :value=>[[-1, 6], [-1, 5], [-1, 4], [-2, 6], [-2, 5], [-2, 4], [-3, 6], [-3, 5], [-3, 4]],
96
+ # :args=>[[-2, 5]],
97
+ # :val=> [[-1, 6], [-1, 5], [-1, 4], [-2, 6], [-2, 5], [-2, 4], [-3, 6], [-3, 5], [-3, 4]],
98
+ # :failure=>:check_failed}}
99
+ # => {:total=>1, :check_failed=>1}
100
+ ```
63
101
 
64
- Clojure uses Symbols to refer to functions. To refer to a method in Ruby, we must use the `method` method.
102
+ Great, it's found a problem! This is saying that an input case was generated that failed our spec. The `:problems` values let's us know exactly what it found wrong. It's saying that the `:ret` part of our `neighbours` spec failed the 'Set' predicate. We can see that the value at `:val` is an Array, not a Set! There's our problem! Let's fix it by calling `to_set` before we return the collection of cells:
65
103
 
66
- ```rb
67
- def self.hello(name)
68
- "Hello #{name}"
104
+ ```ruby
105
+ def self.neighbours(cell)
106
+ cell_x, cell_y = cell
107
+ (-1..1).to_a.repeated_permutation(2).map { |(x, y)| [cell_x - x, cell_y - y] }.to_set
69
108
  end
70
109
 
71
- S.fdef(method(:hello), :args => S.cat(:name => String), :ret => String)
110
+ S::Test.summarize_results S::Test.check(method(:neighbours))
111
+ # {:spec=>"Speculation::FSpec(main.neighbours)",
112
+ # :method=>#<Method: main.neighbours>,
113
+ # :failure=>
114
+ # {:problems=>
115
+ # [{:path=>[:ret],
116
+ # :pred=>
117
+ # [#<Method: Speculation::Predicates.count_eq?>,
118
+ # [8, #<Set: {[-4, -1], [-4, -2], [-4, -3], [-5, -1], [-5, -2], [-5, -3], [-6, -1], [-6, -2], [-6, -3]}>]],
119
+ # :val=>
120
+ # #<Set: {[-4, -1], [-4, -2], [-4, -3], [-5, -1], [-5, -2], [-5, -3], [-6, -1], [-6, -2], [-6, -3]}>,
121
+ # :via=>[],
122
+ # :in=>[]}],
123
+ # :spec=>Speculation::EverySpec(),
124
+ # :value=>
125
+ # #<Set: {[-4, -1], [-4, -2], [-4, -3], [-5, -1], [-5, -2], [-5, -3], [-6, -1], [-6, -2], [-6, -3]}>,
126
+ # :args=>[[-5, -2]],
127
+ # :val=>
128
+ # #<Set: {[-4, -1], [-4, -2], [-4, -3], [-5, -1], [-5, -2], [-5, -3], [-6, -1], [-6, -2], [-6, -3]}>,
129
+ # :failure=>:check_failed}}
130
+ # => {:total=>1, :check_failed=>1}
72
131
  ```
73
132
 
74
- #### Block args
133
+ So we've fixed our Set problem, but now we have another. The `:ret` spec has failed once again, but this time the `:pred` is `Speculation::Predicates.count_eq?`, with an argument of 8 and then a set of 9 cells. Aha! We're including the given cell in this set of neighbours, therefore getting one too neighbouring cells back! That's an easy fix:
75
134
 
76
- In addition to regular arguments which can easily be described as a list, Ruby methods can take blocks. In Speculation, we spec a method's block separately to its args:
77
-
78
- ```rb
79
- def self.hello(name, &block)
80
- "Hello #{block.call(name)}"
135
+ ```ruby
136
+ def self.neighbours(cell)
137
+ cell_x, cell_y = cell
138
+ block = (-1..1).to_a.repeated_permutation(2).map { |(x, y)| [cell_x - x, cell_y - y] }.to_set
139
+ block - Set[cell]
81
140
  end
82
141
 
83
- S.fdef(method(:hello), :args => S.cat(:name => String),
84
- :block => S.fspec(:args => S.cat(:s => String), :ret => String),
85
- :ret => String)
142
+ S::Test.summarize_results S::Test.check(method(:neighbours))
143
+ # => {:total=>1, :check_passed=>1}
86
144
  ```
87
145
 
88
- #### Generators and quick check
146
+ Great, we've managed to verify that, after generating many random inputs (1,000 by default), our method's return value satisfies the properties we defined in its spec. That gives me more confidence than a small handful of hand-written example tests would!
147
+
148
+ We can gain additional leverage from our spec: we can [`instrument`][instrument] the `neighbours` method so that it lets us know when it's been invoked with arguments that do not conform to its `:args` spec.
149
+
150
+ Before we do that, let's observe the method's current behavior when we provide deceptively invalid arguments:
151
+
152
+ ```ruby
153
+ neighbours([1.0, 2.0])
154
+ # => #<Set: {[2.0, 3.0], [2.0, 2.0], [2.0, 1.0], [1.0, 3.0], [1.0, 1.0], [0.0, 3.0], [0.0, 2.0], [0.0, 1.0]}>
155
+ ```
156
+
157
+ Here, we've provided a tuple of floats as a coordinate. This didn't raise any errors with our current implementation; it has dutifully gone to work and returned a value. However, the return value doesn't make sense: our program deals with integer pair coordinates. This situation would most likely lead to either invalid data or an exception at a later stage in our program, far away from the root cause of the problem (calling a method with incorrect types).
158
+
159
+ Let's address that by instrumenting our method so that it verifies its arguments at invocation time:
160
+
161
+ ```ruby
162
+ S::Test.instrument method(:neighbours)
163
+ neighbours([1.0, 2.0])
164
+ # Speculation::Error: Call to 'main.neighbours' did not conform to spec:
165
+ # In: [0, 1] val: 2.0 fails spec: :"gol/coordinate" at: [:args, :cell, 1] predicate: [Integer, [2.0]]
166
+ # In: [0, 0] val: 1.0 fails spec: :"gol/coordinate" at: [:args, :cell, 0] predicate: [Integer, [1.0]]
167
+ # :spec Speculation::RegexSpec()
168
+ # :value [[1.0, 2.0]]
169
+ # :args [[1.0, 2.0]]
170
+ # :failure :instrument
171
+ # :caller "(pry):44:in `<main>'"
172
+ # from /Users/jamie/Projects/speculation/lib/speculation/test.rb:237:in `block in spec_checking_fn'
173
+ ```
89
174
 
90
- Speculation uses [`Rantly`](https://github.com/abargnesi/rantly) for random data generation. Generator functions in Speculation are Procs that take one argument (Rantly instance) and return a random value. While Clojure's test.check generators generate values that start small and continue to grow and get more complex as a property holds true, Rantly always generates random values.
175
+ We can see from the error message that our argument has two problems: both elements of our array argument have failed the Integer predicate for the `:"gol/coordinate"` spec. This is arguably much better feedback than if we hadn't instrumented this method.
91
176
 
92
- Rantly gives Speculation the ability to shrink a failing test case down to its smallest failing case, however in Speculation we limit this to Integers and Strings. This is an area where Speculation may currently be significantly weaker than clojure.spec.
177
+ We've demonstrated several Speculation features, so we'll leave this demo here. See the full [Game of Life example](examples/game_of_life.rb) where we take this idea further.
178
+
179
+ ## Examples
180
+
181
+ * [sinatra-web-app](examples/sinatra-web-app): A small Sinatra web application demonstrating parameter validation and API error message generation.
182
+ * [spec_guide.rb](examples/spec_guide.rb): Speculation port of Clojure's [spec guide](https://clojure.org/guides/spec), demonstrating most features.
183
+ * [codebreaker.rb](examples/codebreaker.rb): Speculation port of the 'codebreaker' game described in [Interactive development with clojure.spec](http://blog.cognitect.com/blog/2016/10/5/interactive-development-with-clojurespec).
184
+ * [game_of_life.rb](examples/game_of_life.rb): [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) implementation.
185
+ * [json_parser.rb](examples/json_parser.rb): (toy) JSON parser using Speculation.
93
186
 
94
187
  ## Project status
95
188
 
@@ -97,10 +190,8 @@ Speculation will mirror any changes made to clojure.spec. clojure.spec is still
97
190
 
98
191
  While most of features of clojure.spec are implemented in Speculation, a few remain:
99
192
 
100
- - [`unform`](https://clojuredocs.org/clojure.spec/unform)
101
- - [`form`](https://clojuredocs.org/clojure.spec/form)
102
- - [`abbrev`](https://clojuredocs.org/clojure.spec/abbrev)
103
- - [`describe`](https://clojuredocs.org/clojure.spec/describe)
193
+ - [`multi-spec`](https://clojure.github.io/clojure/branch-master/clojure.spec-api.html#clojure.spec/multi-spec) - Ruby doesn't have an equivalent of multimethods...
194
+ - [`describe`](https://clojuredocs.org/clojure.spec/describe) - Since we can't capture the source code of a Ruby method/proc, we won't be able to match Clojure's `s/describe` but we may be able to come up with something close.
104
195
 
105
196
  ## Improvements
106
197
 
@@ -114,7 +205,7 @@ Some things I hope to focus on in the near future:
114
205
 
115
206
  ## Development
116
207
 
117
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake` to run rubocop and the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
208
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake` to run Rubocop and the test suite. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
118
209
 
119
210
  ## Contributing
120
211
 
@@ -123,3 +214,13 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/englis
123
214
  ## License
124
215
 
125
216
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
217
+
218
+ [def]: http://www.rubydoc.info/github/english/speculation/master/Speculation#def-class_method
219
+ [coll_of]: http://www.rubydoc.info/github/english/speculation/master/Speculation#coll_of-class_method
220
+ [tuple]: http://www.rubydoc.info/github/english/speculation/master/Speculation#tuple-class_method
221
+ [int_in]: http://www.rubydoc.info/github/english/speculation/master/Speculation#int_in-class_method
222
+ [ns-symbols]: https://github.com/english/speculation/wiki/Namespaced-Symbols
223
+ [speculation]: http://www.rubydoc.info/github/english/speculation/master/Speculation
224
+ [gen]: http://www.rubydoc.info/github/english/speculation/master/Speculation/Gen
225
+ [test]: http://www.rubydoc.info/github/english/speculation/master/Speculation/Test
226
+ [instrument]: http://www.rubydoc.info/github/english/speculation/master/Speculation/Test#instrument-class_method
@@ -72,7 +72,7 @@ def self.score(secret, guess)
72
72
  end
73
73
 
74
74
  STest.check method(:score)
75
- # [{:spec=>Speculation::FSpec(main.score), :"Speculation::Test/ret"=>{:num_tests=>1000, :result=>true}, :method=>#<Method: main.score>}]
75
+ # [{:spec=>Speculation::FSpec(main.score), :ret=>{:num_tests=>1000, :result=>true}, :method=>#<Method: main.score>}]
76
76
 
77
77
  def self.score(secret, guess)
78
78
  { ns(:exact_matches) => 4,
@@ -82,12 +82,12 @@ end
82
82
  # STest.check method(:score)
83
83
 
84
84
  # [{:spec=>Speculation::FSpec(main.score),
85
- # :"Speculation::Test/ret"=>
85
+ # :ret=>
86
86
  # {:fail=>[[:y, :b, :r, :r, :b], [:y, :w, :g, :r, :g]],
87
87
  # :block=>nil,
88
88
  # :num_tests=>1,
89
89
  # :result=>
90
- # #<Speculation::Error: {:"Speculation/problems"=>
90
+ # #<Speculation::Error: {:problems=>
91
91
  # [{:path=>[:fn],
92
92
  # :val=>
93
93
  # {:args=>{:secret=>[:y, :b, :r, :r, :b], :guess=>[:y, :w, :g, :r, :g]},
@@ -109,7 +109,7 @@ S.exercise_fn method(:score)
109
109
 
110
110
  STest.check method(:score)
111
111
 
112
- # [{:spec=>Speculation::FSpec(main.score), :"Speculation::Test/ret"=>{:num_tests=>1000, :result=>true}, :method=>#<Method: main.score>}]
112
+ # [{:spec=>Speculation::FSpec(main.score), :ret=>{:num_tests=>1000, :result=>true}, :method=>#<Method: main.score>}]
113
113
 
114
114
  def self.score(secret, guess)
115
115
  { ns(:exact_matches) => exact_matches(secret, guess),
@@ -144,7 +144,7 @@ S.exercise_fn method(:exact_matches)
144
144
  # [[[:r, :b, :r, :c], [:y, :r, :g, :b]], nil, 0],
145
145
 
146
146
  STest.check method(:exact_matches)
147
- # [{:spec=>Speculation::FSpec(main.exact_matches), :"Speculation::Test/ret"=>{:num_tests=>1000, :result=>true}, :method=>#<Method: main.exact_matches>}]
147
+ # [{:spec=>Speculation::FSpec(main.exact_matches), :ret=>{:num_tests=>1000, :result=>true}, :method=>#<Method: main.exact_matches>}]
148
148
 
149
149
  STest.instrument method(:exact_matches)
150
150
  S.exercise_fn method(:score)
@@ -163,7 +163,7 @@ end
163
163
  # S.exercise_fn method(:score)
164
164
 
165
165
  # Speculation::Error: Call to 'main.exact_matches' did not conform to spec:
166
- # In: [1] val: [:w, :y, :c] fails spec: :"Object/code" at: [:args, :guess] predicate: [#<Method: Speculation::Utils.count_between?>, [[:w, :y, :c], 4, 6]]
166
+ # In: [1] val: [:w, :y, :c] fails spec: :"Object/code" at: [:args, :guess] predicate: [#<Method: Speculation::Predicates.count_between?>, [4, 6, [:w, :y, :c]]]
167
167
  # Speculation/args [[:r, :b, :c, :y, :b, :r], [:w, :y, :c]]
168
168
  # Speculation/failure :instrument
169
169
  # Speculation::Test/caller "(pry):69:in `score'"
@@ -189,7 +189,7 @@ S.exercise_fn method(:exact_matches), 10, S.get_spec(method(:match_count))
189
189
 
190
190
  STest.check_method method(:exact_matches), S.get_spec(method(:match_count))
191
191
 
192
- # {:spec=>Speculation::FSpec(main.match_count), :"Speculation::Test/ret"=>{:num_tests=>1000, :result=>true}, :method=>#<Method: main.exact_matches>}
192
+ # {:spec=>Speculation::FSpec(main.match_count), :ret=>{:num_tests=>1000, :result=>true}, :method=>#<Method: main.exact_matches>}
193
193
 
194
194
  STest.instrument method(:exact_matches), :spec => { method(:exact_matches) => S.get_spec(method(:match_count)) }
195
195
 
@@ -202,7 +202,7 @@ S.exercise_fn method(:score)
202
202
 
203
203
  STest.check method(:score)
204
204
 
205
- # [{:spec=>Speculation::FSpec(main.score), :"Speculation::Test/ret"=>{:num_tests=>1000, :result=>true}, :method=>#<Method: main.score>}]
205
+ # [{:spec=>Speculation::FSpec(main.score), :ret=>{:num_tests=>1000, :result=>true}, :method=>#<Method: main.score>}]
206
206
 
207
207
  def self.all_matches(secret, guess)
208
208
  frequencies = ->(xs) { xs.group_by(&:itself).transform_values(&:count) }
@@ -0,0 +1,120 @@
1
+ require "bundler/setup"
2
+ require "speculation"
3
+ require "speculation/gen"
4
+ require "speculation/test"
5
+
6
+ S = Speculation
7
+ STest = S::Test
8
+ Gen = S::Gen
9
+
10
+ S.def :"gol/coordinate", S.with_gen(Integer) { S.gen(S.int_in(-5..5)) }
11
+ S.def :"gol/cell", S.tuple(:"gol/coordinate", :"gol/coordinate")
12
+ S.def :"gol/world", S.coll_of(:"gol/cell", :kind => Set)
13
+
14
+ def self.neighbours(cell)
15
+ cell_x, cell_y = cell
16
+ (-1..1).repeated_permutation(2).map { |(x, y)| [cell_x - x, cell_y - y] }.to_set
17
+ block - Set[cell]
18
+ end
19
+
20
+ def self.cell_distance(cell_a, cell_b)
21
+ [
22
+ (cell_a[0] - cell_b[0]).abs,
23
+ (cell_a[1] - cell_b[1]).abs
24
+ ]
25
+ end
26
+
27
+ S.fdef method(:neighbours),
28
+ :args => S.cat(:cell => :"gol/cell"),
29
+ :ret => S.coll_of(:"gol/cell", :count => 8, :kind => Set),
30
+ :fn => ->(fn) {
31
+ cell = fn[:args][:cell]
32
+ neighbours = fn[:ret]
33
+ neighbours.all? { |neighbour| [[1, 0], [0, 1], [1, 1]].include?(cell_distance(cell, neighbour)) }
34
+ }
35
+
36
+ S.exercise_fn(method(:neighbours))
37
+ STest.instrument(method(:neighbours))
38
+ # STest.summarize_results STest.check(method(:neighbours))
39
+
40
+ def self.alive_neighbours(world, cell)
41
+ neighbours(cell).intersection(world)
42
+ end
43
+
44
+ def self.alive?(world, cell)
45
+ world.include?(cell)
46
+ end
47
+
48
+ def self.tick(world)
49
+ world.
50
+ flat_map { |cell| neighbours(cell).to_a }.
51
+ select { |cell|
52
+ alive_neighbour_count = alive_neighbours(world, cell).count
53
+
54
+ if alive?(world, cell)
55
+ (2..3).cover?(alive_neighbour_count)
56
+ else
57
+ alive_neighbour_count == 3
58
+ end
59
+ }.
60
+ to_set
61
+ end
62
+
63
+ S.fdef method(:tick),
64
+ :args => S.cat(:world => :"gol/world"),
65
+ :ret => :"gol/world"
66
+
67
+ S.exercise_fn(method(:tick))
68
+ STest.instrument(method(:tick))
69
+ # STest.check(method(:tick))
70
+
71
+ def self.simulation(world)
72
+ Enumerator.new do |yielder|
73
+ loop do
74
+ yielder << world
75
+ world = tick(world)
76
+ end
77
+ end
78
+ end
79
+
80
+ def self.serialize_world(world)
81
+ return [[]] if world.empty?
82
+
83
+ min_y, max_y = world.minmax_by(&:last).map(&:last)
84
+ min_x, max_x = world.minmax_by(&:first).map(&:first)
85
+
86
+ (min_y.pred..max_y.next).map { |y| (min_x.pred..max_x.next).map { |x| world.include?([x, y]) } }
87
+ end
88
+
89
+ S.fdef method(:serialize_world),
90
+ :args => S.cat(:world => :"gol/world"),
91
+ :ret => S.coll_of(S.coll_of(:"Speculation/boolean")),
92
+ :fn => ->(fn) { fn[:ret].flatten.count(&:itself) == fn[:args][:world].count }
93
+
94
+ def self.print_world(world, out)
95
+ world.each do |line|
96
+ line.each do |cell|
97
+ if cell
98
+ out << "\u2588"
99
+ else
100
+ out << "\u2591"
101
+ end
102
+ end
103
+
104
+ out << "\n"
105
+ end
106
+
107
+ nil
108
+ end
109
+
110
+ init = Gen.generate(S.gen(:"gol/world"))
111
+ worlds = simulation(init).lazy.take_while { |world| !world.empty? }
112
+
113
+ worlds.first(10).each do |world|
114
+ print "\033[2J"
115
+ puts world.sort.to_s
116
+ print_world(serialize_world(world), STDOUT)
117
+ sleep 0.5
118
+ end
119
+
120
+ # STest.summarize_results STest.check