moory 0.6.0 → 1.0.0
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/Gemfile.lock +1 -1
- data/README.md +0 -200
- data/examples/ab_star.rb +12 -8
- data/examples/decoder.rb +16 -0
- data/lib/moory.rb +9 -4
- data/lib/moory/decoder.rb +28 -16
- data/lib/moory/loader.rb +11 -0
- data/lib/moory/machine.rb +93 -0
- data/lib/moory/recogniser.rb +32 -0
- data/lib/moory/refinement.rb +41 -0
- data/lib/moory/repertoire.rb +68 -0
- data/lib/moory/ruleparser.rb +82 -0
- data/lib/moory/transitions.rb +115 -0
- data/lib/moory/version.rb +1 -1
- metadata +10 -13
- data/examples/readme_example.rb +0 -38
- data/examples/silly.rb +0 -34
- data/examples/single_as_and_bs.rb +0 -21
- data/examples/well_mannered_objects.rb +0 -81
- data/images/ab_star.png +0 -0
- data/images/foo_bar_etiquette.png +0 -0
- data/lib/moory/acceptor.rb +0 -21
- data/lib/moory/arrow.rb +0 -12
- data/lib/moory/interpreter.rb +0 -109
- data/lib/moory/parser.rb +0 -127
- data/lib/moory/wellmannered.rb +0 -54
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7f7daefff33b5cf15d86669bbaff8c8a8507d54259ac1b4199a8f173aef7bd38
|
|
4
|
+
data.tar.gz: 2d3cf62ea14e5ef8d3164ff5bbeeb84f415c806bdf7f1da63990a1fa53da4553
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d7b95ea00e217342548717c61b75909c63e610110a2597f1d0b78c78b25c171d893d9a9b743799a206a78cdeb748df25d1f636892e59fe570d62040de7695928
|
|
7
|
+
data.tar.gz: c54777928180b9365eabef5ef140b75d67cbd5401483a046862b31a339f588a90a9894e6cdcbf4af5d8364b9dea9455fa18b929303ae2ed3727835166c6470cb
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -1,206 +1,6 @@
|
|
|
1
1
|
# Moory
|
|
2
2
|
Welcome to the Moory gem!
|
|
3
3
|
|
|
4
|
-
You can use this gem to create various kinds of finite machines, and even teach objects manners using a simple plain-text specification language.
|
|
5
|
-
|
|
6
|
-
The [wiki](https://github.com/elclavijero/moory/wiki) is where to go if you are for looking for tutorial material. Perhaps also look in the examples directory.
|
|
7
|
-
|
|
8
|
-
Until you do, here are two examples: the first is an acceptor, and the second is a **well mannered** object.
|
|
9
|
-
|
|
10
|
-
## Example: An acceptor for ab*
|
|
11
|
-
|
|
12
|
-
### Motivation
|
|
13
|
-
|
|
14
|
-
Imagine that you want to create an incredible machine. One capable of determining whether a given character string belongs to the language described by the regular expression `ab*`. You're not going to use a Regexp. That would be far too easy. Instead you derive from the regular expression a Deterministic Finite Automaton (DFA) that does the job. Here's what it might look like:
|
|
15
|
-
|
|
16
|
-

|
|
17
|
-
|
|
18
|
-
Of course, you've set up and solved a system of equations verifying that the design is correct. Now how do you implement this in Ruby? You won't bother trying to apply the State design pattern, because it is a pain. Instead, you'll employ Moory, which is one of the 200+ gems that exist to make creating finite machines easy.
|
|
19
|
-
|
|
20
|
-
### Implementing the Acceptor
|
|
21
|
-
|
|
22
|
-
The first step is to install the Moory gem, achieved by issuing the following command:
|
|
23
|
-
|
|
24
|
-
```
|
|
25
|
-
gem install moory
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
When that's done, start up `irb` and paste the following:
|
|
29
|
-
|
|
30
|
-
```ruby
|
|
31
|
-
require 'moory'
|
|
32
|
-
|
|
33
|
-
ab_star = Moory::Acceptor.create(
|
|
34
|
-
initial: '0',
|
|
35
|
-
transitions: %q{
|
|
36
|
-
0 : a : 1
|
|
37
|
-
0 : b : 2
|
|
38
|
-
1 : a : 2
|
|
39
|
-
1 : b : 1
|
|
40
|
-
2 : a : 2
|
|
41
|
-
2 : b : 2
|
|
42
|
-
},
|
|
43
|
-
final: %w{ 1 }
|
|
44
|
-
)
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
I'll explain the syntax of the transitions later (see the [wiki](https://github.com/elclavijero/moory/wiki)) but you have done enough to confidently type:
|
|
48
|
-
|
|
49
|
-
```ruby
|
|
50
|
-
ab_star.accepts?(string: "ab")
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
and be delighted to see that the answer is `true`.
|
|
54
|
-
|
|
55
|
-
If you are naturally curious, then you might want to put `ab_star` through its paces with further candidates:
|
|
56
|
-
|
|
57
|
-
```ruby
|
|
58
|
-
ab_star.accepts?(string: "abbb") # => true
|
|
59
|
-
ab_star.accepts?(string: "aab") # => false
|
|
60
|
-
ab_star.accepts?(string: "aba") # => false
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
You aren't stuck with testing the strings against the initial state. You can ask the machine to begin its match in any state:
|
|
64
|
-
|
|
65
|
-
```ruby
|
|
66
|
-
ab_star.accepts?(string: "bbb", in_state: '1')
|
|
67
|
-
# => true
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
But what about including characters that don't belong to the machine's alphabet?
|
|
71
|
-
|
|
72
|
-
```ruby
|
|
73
|
-
ab_star.accepts?(string: "bbc", in_state: '1')
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
Well this one will be unceremoniously rejected with a runtime error (unless you are using version 0.1.0, where I forgot to handle bad input, sorry!). Incidentally, if you want to change that behaviour, you should override the `default_proc`.
|
|
77
|
-
```ruby
|
|
78
|
-
ab_star.default_proc = proc { |msg| puts "I'm going to ignore that #{msg}" }
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Now look
|
|
82
|
-
```ruby
|
|
83
|
-
ab_star.accepts?(string: "abcbb")
|
|
84
|
-
# I'm going to ignore that c
|
|
85
|
-
# => true
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
That's better.
|
|
89
|
-
|
|
90
|
-
## Example: A well mannered object
|
|
91
|
-
If you haven't already, open up `irb` and create an uncouth object
|
|
92
|
-
|
|
93
|
-
```ruby
|
|
94
|
-
uncouth = Object.new
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
and teach it how to speak
|
|
98
|
-
|
|
99
|
-
```ruby
|
|
100
|
-
def uncouth.say_foo
|
|
101
|
-
p 'foo'
|
|
102
|
-
end
|
|
103
|
-
def uncouth.say_bar
|
|
104
|
-
p 'bar'
|
|
105
|
-
end
|
|
106
|
-
def uncouth.say_something_nice(name)
|
|
107
|
-
p "Hello, #{name}. You look nice."
|
|
108
|
-
end
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
Do you see anything wrong here? Doesn't that object seem a little dangerous? Everybody knows that `"foo"` comes before `"bar"` in code examples. That absurd object might ruin everything! Is there anything we can do to prevent it from doing the unthinkable?
|
|
112
|
-
|
|
113
|
-
```ruby
|
|
114
|
-
uncouth.say_bar
|
|
115
|
-
uncouth.say_foo
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
Yes. We can filter calls to the uncouth object using Moory's WellMannered class. We need it's methods to be called according to foo-bar-etiquette:
|
|
119
|
-
|
|
120
|
-

|
|
121
|
-
|
|
122
|
-
Let's describe those rules using a language Moory understands:
|
|
123
|
-
|
|
124
|
-
```ruby
|
|
125
|
-
the_rules_of_foo_bar_etiquette = """
|
|
126
|
-
start : say_foo : said_foo
|
|
127
|
-
|
|
128
|
-
said_foo : say_foo : said_foo
|
|
129
|
-
said_foo : say_bar : said_foo_bar
|
|
130
|
-
|
|
131
|
-
said_foo_bar : say_foo : said_foo_bar
|
|
132
|
-
said_foo_bar : say_bar : said_foo_bar
|
|
133
|
-
"""
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
Let's make a postive change, and hereon refer to the uncouth object as the `well_mannered_object`.
|
|
137
|
-
It will be our `protege`:
|
|
138
|
-
|
|
139
|
-
```ruby
|
|
140
|
-
require 'moory'
|
|
141
|
-
|
|
142
|
-
well_mannered_object = Moory::WellMannered.new(
|
|
143
|
-
protege: uncouth,
|
|
144
|
-
rules: the_rules_of_foo_bar_etiquette
|
|
145
|
-
)
|
|
146
|
-
```
|
|
147
|
-
Now let's have a conversation with our protege.
|
|
148
|
-
```ruby
|
|
149
|
-
well_mannered_object.say_something_nice('Adam')
|
|
150
|
-
# "Hello, Adam. You look nice."
|
|
151
|
-
```
|
|
152
|
-
That is nice.
|
|
153
|
-
```ruby
|
|
154
|
-
well_mannered_object.say_bar
|
|
155
|
-
```
|
|
156
|
-
So far so good. Our well mannered object is proving to be polite. If someone asks it to depart from protocol, it will just quietly refuse. But what if that isn't enough? What should it do if someone asks it to do something really bad? The trouble is that well mannered objects are so well-behaved, they won't do depart from the protocol unless we say its OK. Let's do that.
|
|
157
|
-
|
|
158
|
-
```ruby
|
|
159
|
-
well_mannered_object.response_to_rule_breaking = proc { |msg|
|
|
160
|
-
p "You shouldn't ask me to #{msg}! I'm telling!"
|
|
161
|
-
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
Now when we ask it to do something it out of turn
|
|
165
|
-
```ruby
|
|
166
|
-
well_mannered_object.say_bar
|
|
167
|
-
# "You shouldn't ask me to say_bar! I'm telling!"
|
|
168
|
-
```
|
|
169
|
-
it will tell on you. What a nice object! Now don't be such a telltale.
|
|
170
|
-
```ruby
|
|
171
|
-
well_mannered_object.response_to_rule_breaking = nil
|
|
172
|
-
```
|
|
173
|
-
Let's continue
|
|
174
|
-
```ruby
|
|
175
|
-
well_mannered_object.say_bar
|
|
176
|
-
```
|
|
177
|
-
Good. You didn't complain.
|
|
178
|
-
```ruby
|
|
179
|
-
well_mannered_object.say_foo
|
|
180
|
-
# "foo"
|
|
181
|
-
```
|
|
182
|
-
Promising. You haven't yet spoken out of turn.
|
|
183
|
-
```ruby
|
|
184
|
-
well_mannered_object.say_foo
|
|
185
|
-
# "foo"
|
|
186
|
-
```
|
|
187
|
-
A perfectly acceptable repetition.
|
|
188
|
-
```ruby
|
|
189
|
-
well_mannered_object.say_bar
|
|
190
|
-
# "bar"
|
|
191
|
-
```
|
|
192
|
-
Wonderful!
|
|
193
|
-
```ruby
|
|
194
|
-
well_mannered_object.say_something_nice('Adam')
|
|
195
|
-
# "Hello, Adam. You look nice."
|
|
196
|
-
```
|
|
197
|
-
Yes. Thank you.
|
|
198
|
-
|
|
199
|
-
### Before you go...
|
|
200
|
-
|
|
201
|
-
There's more to Moory than its Acceptors and WellMannered objects. I'll show you how to use its other features in the [wiki](https://github.com/elclavijero/moory/wiki).
|
|
202
|
-
|
|
203
|
-
|
|
204
4
|
## Installation
|
|
205
5
|
|
|
206
6
|
Add this line to your application's Gemfile:
|
data/examples/ab_star.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
require 'moory'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
rcg = Moory::Recogniser.new(
|
|
4
4
|
initial: '0',
|
|
5
|
-
|
|
5
|
+
rules: %q{
|
|
6
6
|
0 : a : 1
|
|
7
7
|
0 : b : 2
|
|
8
8
|
1 : a : 2
|
|
@@ -10,11 +10,15 @@ ab_star = Moory::Acceptor.create(
|
|
|
10
10
|
2 : a : 2
|
|
11
11
|
2 : b : 2
|
|
12
12
|
},
|
|
13
|
-
final:
|
|
13
|
+
final: [
|
|
14
|
+
'1'
|
|
15
|
+
]
|
|
14
16
|
)
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
# =>
|
|
19
|
-
|
|
20
|
-
# => false
|
|
18
|
+
pp rcg.accepts?("a") # => true
|
|
19
|
+
pp rcg.accepts?("b") # => false
|
|
20
|
+
pp rcg.accepts?("c") # => false
|
|
21
|
+
pp rcg.accepts?("ab") # => true
|
|
22
|
+
pp rcg.accepts?("ac") # => false
|
|
23
|
+
pp rcg.accepts?("abb") # => true
|
|
24
|
+
pp rcg.accepts?("abc") # => false
|
data/examples/decoder.rb
ADDED
data/lib/moory.rb
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
require "moory/version"
|
|
2
|
-
require "moory/
|
|
3
|
-
require "moory/
|
|
4
|
-
require "moory/
|
|
2
|
+
require "moory/refinement"
|
|
3
|
+
require "moory/pair"
|
|
4
|
+
require "moory/machine"
|
|
5
|
+
require "moory/transitions"
|
|
6
|
+
require "moory/repertoire"
|
|
7
|
+
|
|
8
|
+
require "moory/ruleparser"
|
|
9
|
+
require "moory/loader"
|
|
10
|
+
require "moory/recogniser"
|
|
5
11
|
require "moory/decoder"
|
|
6
|
-
require "moory/wellmannered"
|
|
7
12
|
|
|
8
13
|
module Moory
|
|
9
14
|
# Your code goes here...
|
data/lib/moory/decoder.rb
CHANGED
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
module Moory
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
2
|
+
class Decoder
|
|
3
|
+
include Efferent
|
|
4
|
+
|
|
5
|
+
def initialize(rules:, initial:, ostream:$stdout)
|
|
6
|
+
@initial = initial
|
|
7
|
+
@state = initial
|
|
8
|
+
@ostream = ostream
|
|
9
|
+
configure(rules)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Decode a string according to the rules. Writes the decoded string to the output
|
|
13
|
+
# stream configured at initialisation (which is $stdout, by default). Characters
|
|
14
|
+
# not belonging to the alphabet will be dropped.
|
|
15
|
+
#
|
|
16
|
+
# @param string [String] the string you wish to decode
|
|
17
|
+
def decode(string)
|
|
18
|
+
string.each_char { |c| issue(c) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def configure(rules)
|
|
24
|
+
Loader.load(rules: rules, machine: self)
|
|
25
|
+
repertoire.always = method(:write)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def write(output=nil)
|
|
29
|
+
@ostream.write(output) if output
|
|
18
30
|
end
|
|
19
31
|
end
|
|
20
32
|
end
|
data/lib/moory/loader.rb
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
require 'forwardable'
|
|
2
|
+
|
|
3
|
+
module Moory
|
|
4
|
+
module Afferent
|
|
5
|
+
attr_accessor :transitions
|
|
6
|
+
attr_accessor :state
|
|
7
|
+
|
|
8
|
+
extend Forwardable
|
|
9
|
+
def_delegators :@transitions, :states, :alphabet
|
|
10
|
+
|
|
11
|
+
def transitions
|
|
12
|
+
@transitions ||= Moory::Transition::Storage.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Issue a stimulus to the machine.
|
|
16
|
+
#
|
|
17
|
+
# @return will either be the new `settlement`, or `nil` if the stimulus is not understood.
|
|
18
|
+
def issue(stimulus)
|
|
19
|
+
if response = transitions.response(origin: state, stimulus: stimulus)
|
|
20
|
+
honour(response)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Reveals the stimuli to which the machine may respond in its current state.
|
|
25
|
+
#
|
|
26
|
+
# @return [Set]
|
|
27
|
+
def awaits
|
|
28
|
+
transitions.egresses(state:state)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Answers whether a machine can respond to the given stimlus in its current state.
|
|
32
|
+
#
|
|
33
|
+
# @return [Boolean]
|
|
34
|
+
def understand?(stimulus)
|
|
35
|
+
awaits.include?(stimulus)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def settle_accordingly(response)
|
|
41
|
+
@state = response[:settlement]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def honour(response)
|
|
45
|
+
settle_accordingly(response)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
module Efferent
|
|
50
|
+
include Afferent
|
|
51
|
+
|
|
52
|
+
attr_accessor :repertoire
|
|
53
|
+
|
|
54
|
+
extend Forwardable
|
|
55
|
+
def_delegators :@repertoire, :always=, :always
|
|
56
|
+
def_delegators :@repertoire, :fallback=, :fallback
|
|
57
|
+
|
|
58
|
+
def repertoire
|
|
59
|
+
@repertoire ||= Moory::Repertoire.new
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def honour(response)
|
|
65
|
+
perform(response) if repertoire
|
|
66
|
+
super
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def perform(response)
|
|
70
|
+
perform_always(response[:output])
|
|
71
|
+
perform_special(response)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def perform_always(output)
|
|
75
|
+
guarded_call(always, output)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def perform_special(response)
|
|
79
|
+
guarded_call(
|
|
80
|
+
repertoire.recall(response[:effector]),
|
|
81
|
+
response[:output]
|
|
82
|
+
) if response[:effector]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def guarded_call(receiver, output)
|
|
86
|
+
output ? receiver.call(output) : receiver.call
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
class Transducer
|
|
91
|
+
include Efferent
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Moory
|
|
2
|
+
class Recogniser
|
|
3
|
+
include Afferent
|
|
4
|
+
|
|
5
|
+
def initialize(rules:, initial:, final:)
|
|
6
|
+
@initial = initial
|
|
7
|
+
@final = final
|
|
8
|
+
Loader.load(rules: rules, machine: self)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Answers whether the given string is accepted; that is, does it belong to
|
|
12
|
+
# the language described by the rules?
|
|
13
|
+
#
|
|
14
|
+
# @param string [String] the candidate string.
|
|
15
|
+
# @return [Boolean] true if the string is accepted; false, otherwise.
|
|
16
|
+
def accepts?(string)
|
|
17
|
+
reset
|
|
18
|
+
|
|
19
|
+
string.each_char.all? { |c| issue(c) } && accepting?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def reset
|
|
25
|
+
@state = @initial
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def accepting?
|
|
29
|
+
@final.include?(state)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Moory
|
|
2
|
+
module Refinement
|
|
3
|
+
module ArrayRefinement
|
|
4
|
+
refine Array do
|
|
5
|
+
def map_to(other)
|
|
6
|
+
Hash[*self.zip(other).flatten].compact
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def identity_map
|
|
10
|
+
map_to(self)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module HashRefinement
|
|
16
|
+
refine Hash do
|
|
17
|
+
using ArrayRefinement
|
|
18
|
+
def domain
|
|
19
|
+
keys.to_set
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def range
|
|
23
|
+
values.to_set
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def composable?(other)
|
|
27
|
+
!(range & other.domain).empty?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def then(other)
|
|
31
|
+
return {} if other.empty?
|
|
32
|
+
transform_values { |v| other[v] }.compact
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def project(*args)
|
|
36
|
+
args.identity_map.then(self)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module Moory
|
|
2
|
+
class Repertoire
|
|
3
|
+
SKIP = proc {}
|
|
4
|
+
|
|
5
|
+
attr_reader :knowledge
|
|
6
|
+
attr_reader :fallback
|
|
7
|
+
attr_reader :always
|
|
8
|
+
|
|
9
|
+
# Create a repertoire from a Hash mapping names to callable objects.
|
|
10
|
+
#
|
|
11
|
+
# @return [Repertoire]
|
|
12
|
+
def Repertoire.from_hash(hash={})
|
|
13
|
+
new.tap { |r| r.cram(hash) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@fallback = SKIP
|
|
18
|
+
@always = SKIP
|
|
19
|
+
@knowledge = {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Learn multiple items of knowledge.
|
|
23
|
+
def cram(hash={})
|
|
24
|
+
hash.each { |k,v| learn(name: k, item: v) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Associate a name with a callable object. The item will not be stored unless it is callable;
|
|
28
|
+
# that is, it must respond to `:call`.
|
|
29
|
+
#
|
|
30
|
+
# @param item the callable object you want to store.
|
|
31
|
+
# @param name the name you will use to identify the `item`.
|
|
32
|
+
def learn(item:,name:)
|
|
33
|
+
@knowledge.store(name, item) if (
|
|
34
|
+
appropriate?(item) && name
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Recall a callable object by name.
|
|
39
|
+
#
|
|
40
|
+
# @param the name identifying the object you want to retrieve.
|
|
41
|
+
# @return if the given value represents knowledge, then that which has been learned is returned.
|
|
42
|
+
# If the name does not represent an item of knowledge, then the `fallback` is returned. You will
|
|
43
|
+
# always get something callable when you call this method.
|
|
44
|
+
def recall(name)
|
|
45
|
+
name ?
|
|
46
|
+
knowledge.fetch(name, fallback) :
|
|
47
|
+
fallback
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def fallback=(obj)
|
|
51
|
+
@fallback = censor(obj)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def always=(obj)
|
|
55
|
+
@always = censor(obj)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def appropriate?(obj)
|
|
61
|
+
obj.respond_to?(:call)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def censor(obj)
|
|
65
|
+
appropriate?(obj) ? obj : SKIP
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
module Moory
|
|
2
|
+
module RuleParser
|
|
3
|
+
RULES = [
|
|
4
|
+
{ origin: 'origin', stimulus: ':', settlement: 'stimulus' },
|
|
5
|
+
|
|
6
|
+
{ origin: 'stimulus', stimulus: ':', settlement: 'settlement' },
|
|
7
|
+
{ origin: 'stimulus', stimulus: '/', settlement: 'output' },
|
|
8
|
+
|
|
9
|
+
{ origin: 'output', stimulus: ':', settlement: 'settlement' },
|
|
10
|
+
{ origin: 'output', stimulus: '/', settlement: 'effector' },
|
|
11
|
+
|
|
12
|
+
{ origin: 'effector', stimulus: ':', settlement: 'settlement' },
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
Transitions = Moory::Transition::Storage.new.tap { |ts|
|
|
16
|
+
RULES.each { |r| ts.store(r) }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class LineReader < Moory::Transducer
|
|
20
|
+
attr_reader :scan_data
|
|
21
|
+
|
|
22
|
+
IGNORE = [ ' ', '\t' ]
|
|
23
|
+
|
|
24
|
+
def initialize
|
|
25
|
+
@transitions = Transitions
|
|
26
|
+
prepare
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def reset
|
|
30
|
+
@state = 'origin'
|
|
31
|
+
@scan_data = {}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def <<(string)
|
|
35
|
+
puts(string)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def puts(string)
|
|
41
|
+
string.each_char { |c| putc(c) }
|
|
42
|
+
scan_data
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
alias special? understand?
|
|
46
|
+
|
|
47
|
+
def putc(char)
|
|
48
|
+
(special?(char) ?
|
|
49
|
+
issue(char) :
|
|
50
|
+
target(state) << char) unless ignore?(char)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def prepare
|
|
54
|
+
reset
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def target(state)
|
|
58
|
+
@scan_data.fetch(state.to_sym) { |k| @scan_data[k] = '' }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def ignore?(char)
|
|
62
|
+
IGNORE.include?(char)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class FileReader
|
|
67
|
+
def initialize
|
|
68
|
+
@line_reader = Moory::RuleParser::LineReader.new
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def analyse(input)
|
|
72
|
+
input
|
|
73
|
+
.each_line
|
|
74
|
+
.reduce([]) do |list, line|
|
|
75
|
+
list << (@line_reader << (line.chomp))
|
|
76
|
+
@line_reader.reset
|
|
77
|
+
list
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
|
|
3
|
+
module Moory
|
|
4
|
+
module Transition
|
|
5
|
+
# Serves as a transition relation.
|
|
6
|
+
class Storage
|
|
7
|
+
def count
|
|
8
|
+
storage.size
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Store a transition described by a hash
|
|
12
|
+
#
|
|
13
|
+
# @param params [Hash] a hash describing a transition. At a minimum,
|
|
14
|
+
# it should include `:origin`, `:stimulus`, and `:settlement`.
|
|
15
|
+
# It may also include `:output` and `:effector`.
|
|
16
|
+
def store(params)
|
|
17
|
+
storage.merge!(Hasher.new(params)) do |key, oldval, newval|
|
|
18
|
+
oldval.merge!(newval)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Retrieve the unique response to a stimulus from a given origin.
|
|
23
|
+
#
|
|
24
|
+
# @return [Hash, nil] if the given parameters represent a transition, a Hash is returned
|
|
25
|
+
# including at least a `:settlement`, and maybe one/both of `:output` and `:effector`.
|
|
26
|
+
# If the paremeters do not correspond to a uniqu transition, `nil` is returned.
|
|
27
|
+
def response(origin:, stimulus:)
|
|
28
|
+
storage.dig(origin, stimulus)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Retrieve the states represented by the transition relation.
|
|
32
|
+
#
|
|
33
|
+
# @return [Set]
|
|
34
|
+
def states
|
|
35
|
+
storage
|
|
36
|
+
.keys
|
|
37
|
+
.to_set
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Retrieve the alphabet
|
|
41
|
+
#
|
|
42
|
+
# @param restrict retricts the alphabet to that subset applicable to the state named by
|
|
43
|
+
# the given value.
|
|
44
|
+
# @return [Set]
|
|
45
|
+
def alphabet(restrict:nil)
|
|
46
|
+
storage
|
|
47
|
+
.select { |k| restrict ? k == restrict : true }
|
|
48
|
+
.values
|
|
49
|
+
.collect { |r| r.keys }
|
|
50
|
+
.flatten
|
|
51
|
+
.to_set
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Retrieve the egresses for the given state.
|
|
55
|
+
#
|
|
56
|
+
# @param state identifies the state for which egresses are being sought.
|
|
57
|
+
# @return [Set]
|
|
58
|
+
def egresses(state:)
|
|
59
|
+
alphabet(restrict: state)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns the transition relation.
|
|
63
|
+
#
|
|
64
|
+
# @return [Hash] represents the transition relation as a mapping from
|
|
65
|
+
# (state, stimulus) to (settlement, output, effector).
|
|
66
|
+
def storage
|
|
67
|
+
@storage ||= {}
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Helps expand a flat-hash description of a transition into a form
|
|
72
|
+
# amenable to storage.
|
|
73
|
+
Hasher = Struct.new(
|
|
74
|
+
:origin,
|
|
75
|
+
:stimulus,
|
|
76
|
+
:settlement,
|
|
77
|
+
:output,
|
|
78
|
+
:effector,
|
|
79
|
+
keyword_init: true
|
|
80
|
+
) do
|
|
81
|
+
|
|
82
|
+
def valid?
|
|
83
|
+
origin && stimulus && settlement
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def to_hash(for_storage: true)
|
|
87
|
+
return {} unless valid?
|
|
88
|
+
|
|
89
|
+
for_storage ? storage_hash : flat_hash
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def storage_hash
|
|
95
|
+
p = Pair.new(left: origin, right: stimulus)
|
|
96
|
+
|
|
97
|
+
p.shunt({
|
|
98
|
+
settlement: settlement,
|
|
99
|
+
output: output,
|
|
100
|
+
effector: effector
|
|
101
|
+
}.compact)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def flat_hash
|
|
105
|
+
{
|
|
106
|
+
origin: origin,
|
|
107
|
+
stimulus: stimulus,
|
|
108
|
+
settlement: settlement,
|
|
109
|
+
output: output,
|
|
110
|
+
effector: effector
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
data/lib/moory/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: moory
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Adam W. Grant
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2018-07-
|
|
11
|
+
date: 2018-07-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -70,21 +70,18 @@ files:
|
|
|
70
70
|
- bin/console
|
|
71
71
|
- bin/setup
|
|
72
72
|
- examples/ab_star.rb
|
|
73
|
-
- examples/
|
|
74
|
-
- examples/silly.rb
|
|
75
|
-
- examples/single_as_and_bs.rb
|
|
76
|
-
- examples/well_mannered_objects.rb
|
|
77
|
-
- images/ab_star.png
|
|
78
|
-
- images/foo_bar_etiquette.png
|
|
73
|
+
- examples/decoder.rb
|
|
79
74
|
- lib/moory.rb
|
|
80
|
-
- lib/moory/acceptor.rb
|
|
81
|
-
- lib/moory/arrow.rb
|
|
82
75
|
- lib/moory/decoder.rb
|
|
83
|
-
- lib/moory/
|
|
76
|
+
- lib/moory/loader.rb
|
|
77
|
+
- lib/moory/machine.rb
|
|
84
78
|
- lib/moory/pair.rb
|
|
85
|
-
- lib/moory/
|
|
79
|
+
- lib/moory/recogniser.rb
|
|
80
|
+
- lib/moory/refinement.rb
|
|
81
|
+
- lib/moory/repertoire.rb
|
|
82
|
+
- lib/moory/ruleparser.rb
|
|
83
|
+
- lib/moory/transitions.rb
|
|
86
84
|
- lib/moory/version.rb
|
|
87
|
-
- lib/moory/wellmannered.rb
|
|
88
85
|
- moory.gemspec
|
|
89
86
|
homepage: https://github.com/elclavijero/moory
|
|
90
87
|
licenses:
|
data/examples/readme_example.rb
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
require 'moory'
|
|
2
|
-
|
|
3
|
-
# Create an acceptor for the language described by ab*
|
|
4
|
-
ab_star = Moory::Acceptor.create(
|
|
5
|
-
initial: '0',
|
|
6
|
-
transitions: %q{
|
|
7
|
-
0 : a : 1
|
|
8
|
-
0 : b : 2
|
|
9
|
-
1 : a : 2
|
|
10
|
-
1 : b : 1
|
|
11
|
-
2 : a : 2
|
|
12
|
-
2 : b : 2
|
|
13
|
-
},
|
|
14
|
-
final: %w{ 1 }
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
# Check some strings
|
|
18
|
-
ab_star.accepts?(string: "ab") # => true
|
|
19
|
-
ab_star.accepts?(string: "abbb") # => true
|
|
20
|
-
ab_star.accepts?(string: "aab") # => false
|
|
21
|
-
ab_star.accepts?(string: "aba") # => false
|
|
22
|
-
|
|
23
|
-
# Check a string against another starting state
|
|
24
|
-
ab_star.accepts?(string: "bbb", in_state: '1')
|
|
25
|
-
# => true
|
|
26
|
-
|
|
27
|
-
# Uncomment the next line, and you'll see a runtime error
|
|
28
|
-
# ab_star.accepts?(string: "bbc", in_state: '1')
|
|
29
|
-
|
|
30
|
-
# Assign a default_proc, which is called when the Acceptor
|
|
31
|
-
# encounters a character not in its alphabet e.g. 'c'
|
|
32
|
-
ab_star.default_proc = proc { |msg| puts "I'm going to ignore that #{msg}" }
|
|
33
|
-
|
|
34
|
-
# Deliberately give the Acceptor a bad character.
|
|
35
|
-
ab_star.accepts?(string: "abcbb")
|
|
36
|
-
# I'm going to ignore that c
|
|
37
|
-
# => true
|
|
38
|
-
|
data/examples/silly.rb
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
require 'moory'
|
|
2
|
-
|
|
3
|
-
silly = Moory::Interpreter.new do |i|
|
|
4
|
-
i.load """
|
|
5
|
-
0 : a / u / foo : 1
|
|
6
|
-
0 : b / v / foo : 2
|
|
7
|
-
1 : a / w / bar : 1
|
|
8
|
-
1 : b / x / bar : 2
|
|
9
|
-
2 : a / y / : 1
|
|
10
|
-
2 : b / z : 2
|
|
11
|
-
"""
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
# Tell the interpreter where we want to start
|
|
15
|
-
silly.state = '0'
|
|
16
|
-
|
|
17
|
-
# Define some handlers for the effectors named in the
|
|
18
|
-
# loaded specification.
|
|
19
|
-
silly.effectors = {
|
|
20
|
-
'foo' => proc { |msg| pp "#{msg} says: fooby fooby foo" },
|
|
21
|
-
'bar' => proc { |msg| pp "#{msg} says: barby barby bar" },
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
# Tell the interpreter the name of the effector we want to use
|
|
25
|
-
# if a transition has output, but no effector.
|
|
26
|
-
silly.fallback_effector = 'bar'
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
# Send the interpreter some messages: putm means "PUT Message"
|
|
30
|
-
silly.putm('b') # outputs: "v says: fooby fooby foo"
|
|
31
|
-
silly.putm('b') # outputs: "z says: barby barby bar"
|
|
32
|
-
silly.putm('a') # outputs: "y says: barby barby bar"
|
|
33
|
-
silly.putm('a') # outputs: "w says: barby barby bar"
|
|
34
|
-
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
require 'moory'
|
|
2
|
-
require 'stringio'
|
|
3
|
-
|
|
4
|
-
ostream = StringIO.new
|
|
5
|
-
|
|
6
|
-
mealy = Moory::Decoder.create(
|
|
7
|
-
initial: '0',
|
|
8
|
-
transitions: %q{
|
|
9
|
-
0 : a / a : 1
|
|
10
|
-
0 : b / b : 2
|
|
11
|
-
1 : a : 1
|
|
12
|
-
1 : b / b : 2
|
|
13
|
-
2 : a / a : 1
|
|
14
|
-
2 : b : 2
|
|
15
|
-
},
|
|
16
|
-
ostream: ostream
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
mealy.decode("abbbbbbaaaabaabbba")
|
|
20
|
-
|
|
21
|
-
ostream.string # => "abababa"
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
# Well-mannered objects
|
|
2
|
-
|
|
3
|
-
# Suppose we have an uncouth object
|
|
4
|
-
uncouth = Object.new
|
|
5
|
-
|
|
6
|
-
# and it can speak
|
|
7
|
-
def uncouth.say_foo
|
|
8
|
-
p 'foo'
|
|
9
|
-
end
|
|
10
|
-
def uncouth.say_bar
|
|
11
|
-
p 'bar'
|
|
12
|
-
end
|
|
13
|
-
def uncouth.say_something_nice(name)
|
|
14
|
-
p "Hello, #{name}. You look nice."
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# Do you see anything wrong here?
|
|
18
|
-
# Doesn't that object seem a little dangerous? Everybody knows that "foo" comes before "bar" in
|
|
19
|
-
# code examples. That absurd object might ruin everything! Is there anything we can do to prevent
|
|
20
|
-
# it from doing the unthinkable?
|
|
21
|
-
#
|
|
22
|
-
# uncouth.say_bar
|
|
23
|
-
# uncouth.say_foo
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
# Yes, of course there is! We can filter calls to the uncouth object using Moory's WellManered
|
|
27
|
-
# class.
|
|
28
|
-
require 'moory'
|
|
29
|
-
|
|
30
|
-
# We can go no further until we write down the rules of foo-bar-etiquette
|
|
31
|
-
|
|
32
|
-
the_rules_of_foo_bar_etiquette = """
|
|
33
|
-
start : say_foo : said_foo
|
|
34
|
-
|
|
35
|
-
said_foo : say_foo : said_foo
|
|
36
|
-
said_foo : say_bar : said_foo_bar
|
|
37
|
-
|
|
38
|
-
said_foo_bar : say_foo : said_foo_bar
|
|
39
|
-
said_foo_bar : say_bar : said_foo_bar
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
|
-
# Let's make a postive change, and hereon refer to the uncouth object as the `well_mannered_object`.
|
|
43
|
-
# It will be our `protege`, and the instantiation of a WellMannered object won't surprise us:
|
|
44
|
-
|
|
45
|
-
well_mannered_object = Moory::WellMannered.new(
|
|
46
|
-
protege: uncouth,
|
|
47
|
-
rules: the_rules_of_foo_bar_etiquette
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
# Now let's have a conversation with our protege.
|
|
51
|
-
well_mannered_object.say_something_nice('Adam')
|
|
52
|
-
# That is nice.
|
|
53
|
-
well_mannered_object.say_bar
|
|
54
|
-
# Good. I'm glad you didn't fall for that
|
|
55
|
-
|
|
56
|
-
# So far so good. Our well mannered object is proving to be polite. If someone asks it
|
|
57
|
-
# to depart from protocol, it will just quietly refuse. But what if that isn't enough?
|
|
58
|
-
# What should it do if someone asks it to do something really bad? The trouble is,
|
|
59
|
-
# well-mannered objects are so well-behaved, they won't do anything unless we say its OK.
|
|
60
|
-
# Let's do that.
|
|
61
|
-
|
|
62
|
-
well_mannered_object.response_to_rule_breaking = proc { |msg|
|
|
63
|
-
pp "You shouldn't ask me to #{msg}! I'm telling!"
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
# Now when we ask it to do something it out of turn
|
|
67
|
-
well_mannered_object.say_bar
|
|
68
|
-
# it will tell on you. What a nice object! Now don't be such a telltale.
|
|
69
|
-
well_mannered_object.response_to_rule_breaking = nil
|
|
70
|
-
# Let's continue
|
|
71
|
-
well_mannered_object.say_bar
|
|
72
|
-
# Good. You didn't complain.
|
|
73
|
-
well_mannered_object.say_foo
|
|
74
|
-
# Promising. You haven't yet spoken out of turn.
|
|
75
|
-
well_mannered_object.say_foo
|
|
76
|
-
# A perfectly acceptable reptition.
|
|
77
|
-
well_mannered_object.say_bar
|
|
78
|
-
# Wonderful!
|
|
79
|
-
well_mannered_object.say_something_nice('Adam')
|
|
80
|
-
# Yes. Thank you.
|
|
81
|
-
|
data/images/ab_star.png
DELETED
|
Binary file
|
|
Binary file
|
data/lib/moory/acceptor.rb
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
module Moory
|
|
2
|
-
module Acceptor
|
|
3
|
-
def Acceptor.create(specification)
|
|
4
|
-
Moory::Interpreter.new do |i|
|
|
5
|
-
i.load(specification[:transitions])
|
|
6
|
-
|
|
7
|
-
i.default_proc = proc { |msg|
|
|
8
|
-
raise "#{i} in state #{i.state} did not understand #{msg}. Input rejected!"
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
define_singleton_method(:accepts?) do |string:, in_state: nil|
|
|
12
|
-
i.state = in_state ? in_state : specification[:initial]
|
|
13
|
-
|
|
14
|
-
string.each_char { |c| putm(c) }
|
|
15
|
-
|
|
16
|
-
specification[:final].include?(i.state)
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
data/lib/moory/arrow.rb
DELETED
data/lib/moory/interpreter.rb
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
require 'set'
|
|
2
|
-
|
|
3
|
-
module Moory
|
|
4
|
-
class Interpreter
|
|
5
|
-
attr_accessor :graph, :effectors, :state
|
|
6
|
-
attr_reader :fallback_effector, :default_proc
|
|
7
|
-
|
|
8
|
-
SKIP = proc {}
|
|
9
|
-
WARN = proc { |msg| warn "Did not understand: #{msg}" }
|
|
10
|
-
|
|
11
|
-
def initialize(graph: {}, effectors: {}, default_proc: SKIP, fallback_always: true, &block)
|
|
12
|
-
@graph = graph
|
|
13
|
-
@effectors = effectors
|
|
14
|
-
@default_proc = default_proc
|
|
15
|
-
@fallback_always = fallback_always
|
|
16
|
-
|
|
17
|
-
instance_eval &block if block_given?
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def load(source)
|
|
21
|
-
p = Moory::Parser.new
|
|
22
|
-
@graph.merge!(p.analyse(source))
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def fallback_effector=(obj)
|
|
26
|
-
candidate = obj.kind_of?(String) ? effectors[obj] : obj
|
|
27
|
-
|
|
28
|
-
@fallback_effector = candidate.respond_to?(:call) ? candidate : nil
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def fallback_always=(bool)
|
|
32
|
-
@fallback_always = bool
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def default_proc=(obj)
|
|
36
|
-
@default_proc = obj.respond_to?(:call) ? obj : nil
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def putm(msg)
|
|
40
|
-
warn """
|
|
41
|
-
#{self} received #{msg} before being assigned a state.
|
|
42
|
-
The message will be passed to the default_proc.
|
|
43
|
-
""" unless state
|
|
44
|
-
|
|
45
|
-
understand?(msg) ? respond(msg) : bad_call(msg)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def understand?(msg)
|
|
49
|
-
receptors.include?(msg)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def receptors
|
|
53
|
-
graph.fetch(state,{}).keys.to_set
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def states
|
|
57
|
-
@states ||= graph.keys.to_set
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def alphabet
|
|
61
|
-
@alphabet ||= Set.new(
|
|
62
|
-
graph.each_value.collect { |m| m.keys }.flatten
|
|
63
|
-
)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
private
|
|
67
|
-
|
|
68
|
-
def respond(msg)
|
|
69
|
-
dispatch(msg)
|
|
70
|
-
resettle(msg)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def dispatch(msg)
|
|
74
|
-
_effector, _output = effector(msg), output(msg)
|
|
75
|
-
|
|
76
|
-
if _effector.respond_to?(:call)
|
|
77
|
-
_effector.arity == 0 ?
|
|
78
|
-
_effector.call :
|
|
79
|
-
_effector.call(_output)
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def resettle(msg)
|
|
84
|
-
@state = graph[state][msg][:state]
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def effector(msg)
|
|
88
|
-
candidate = graph[state][msg][:effector]
|
|
89
|
-
|
|
90
|
-
if candidate.kind_of?(String)
|
|
91
|
-
effectors[candidate]
|
|
92
|
-
else
|
|
93
|
-
candidate || (fallback_effector if fallback_appropriate_for?(msg))
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def fallback_appropriate_for?(msg)
|
|
98
|
-
@fallback_always ? @fallback_always : output(msg)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def output(msg)
|
|
102
|
-
graph[state][msg][:output]
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def bad_call(msg)
|
|
106
|
-
default_proc.call(msg) if default_proc
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
data/lib/moory/parser.rb
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
require 'moory/pair'
|
|
2
|
-
|
|
3
|
-
module Moory
|
|
4
|
-
class Parser
|
|
5
|
-
attr_reader :graph, :staged
|
|
6
|
-
|
|
7
|
-
def initialize
|
|
8
|
-
@graph = {}
|
|
9
|
-
prime_interpreter
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def analyse(input)
|
|
13
|
-
input.each_line do |line|
|
|
14
|
-
scan(line); store if valid?
|
|
15
|
-
reset_interpreter
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
return graph
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
private
|
|
22
|
-
|
|
23
|
-
def scan(string)
|
|
24
|
-
string.chomp.each_char { |c| route(c) }
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def route(char)
|
|
28
|
-
special?(char) ? interpret(char) : write_to_focus(char)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def special?(char)
|
|
32
|
-
interpreter.understand?(char)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def write_to_focus(char)
|
|
36
|
-
staged.fetch(@focus) { |k| staged[k] = '' } << char
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def interpret(char)
|
|
40
|
-
interpreter.putm(char)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def store
|
|
44
|
-
graph.merge!(transition)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def transition
|
|
48
|
-
poise.shunt(response)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def poise
|
|
52
|
-
Moory::Pair.new(
|
|
53
|
-
left: staged['source'],
|
|
54
|
-
right: staged['stimulus']
|
|
55
|
-
)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def response
|
|
59
|
-
{
|
|
60
|
-
state: staged['target'],
|
|
61
|
-
output: staged['output'],
|
|
62
|
-
effector: staged['effector']
|
|
63
|
-
}.compact
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def valid?
|
|
67
|
-
staged['source'] &&
|
|
68
|
-
staged['stimulus'] &&
|
|
69
|
-
staged['target']
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def prime_interpreter
|
|
73
|
-
interpreter.state = '0'
|
|
74
|
-
@staged = {}
|
|
75
|
-
source
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
alias reset_interpreter prime_interpreter
|
|
79
|
-
|
|
80
|
-
def interpreter
|
|
81
|
-
@interpreter ||= Moory::Interpreter.new(config)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def config
|
|
85
|
-
{
|
|
86
|
-
graph: {
|
|
87
|
-
'0' => {
|
|
88
|
-
':' => { state: '1', effector: 'stimulus' },
|
|
89
|
-
' ' => { state: '0' },
|
|
90
|
-
'\t' => { state: '0' }
|
|
91
|
-
},
|
|
92
|
-
'1' => {
|
|
93
|
-
'/' => { state: '2', effector: 'output' },
|
|
94
|
-
':' => { state: '4', effector: 'target' },
|
|
95
|
-
' ' => { state: '1' },
|
|
96
|
-
'\t' => { state: '1' }
|
|
97
|
-
},
|
|
98
|
-
'2' => {
|
|
99
|
-
':' => { state: '4', effector: 'target' },
|
|
100
|
-
'/' => { state: '3', effector: 'effector' },
|
|
101
|
-
' ' => { state: '2' },
|
|
102
|
-
'\t' => { state: '2' }
|
|
103
|
-
},
|
|
104
|
-
'3' => {
|
|
105
|
-
':' => { state: '4', effector: method(:target) },
|
|
106
|
-
' ' => { state: '2' },
|
|
107
|
-
'\t' => { state: '2' }
|
|
108
|
-
},
|
|
109
|
-
'4' => {
|
|
110
|
-
' ' => { state: '4' },
|
|
111
|
-
'\t' => { state: '4' }
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
effectors: {
|
|
115
|
-
'stimulus' => method(:stimulus),
|
|
116
|
-
'output' => method(:output),
|
|
117
|
-
'target' => method(:target),
|
|
118
|
-
'effector' => method(:effector),
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
%w{
|
|
124
|
-
source stimulus target output effector
|
|
125
|
-
}.each { |c| define_method(c) { @focus = c } }
|
|
126
|
-
end
|
|
127
|
-
end
|
data/lib/moory/wellmannered.rb
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
module Moory
|
|
2
|
-
class WellMannered
|
|
3
|
-
attr_reader :response_to_rule_breaking
|
|
4
|
-
|
|
5
|
-
IGNORE = proc {}
|
|
6
|
-
|
|
7
|
-
def initialize(protege:, rules:, initial:'start', response_to_rule_breaking: IGNORE)
|
|
8
|
-
@protege = protege
|
|
9
|
-
@rules = rules
|
|
10
|
-
@initial = initial
|
|
11
|
-
@response_to_rule_breaking = response_to_rule_breaking
|
|
12
|
-
prepare
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def restricted_messages
|
|
16
|
-
interpreter.alphabet
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def response_to_rule_breaking=(obj)
|
|
20
|
-
@response_to_rule_breaking = obj.respond_to?(:call) ? obj : IGNORE
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
def prepare
|
|
26
|
-
interpreter.load(@rules)
|
|
27
|
-
interpreter.state = @initial
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def interpreter
|
|
31
|
-
@interpreter ||= Moory::Interpreter.new
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def method_missing(*args)
|
|
35
|
-
protected?(*args) ?
|
|
36
|
-
filter_first(*args) :
|
|
37
|
-
forward(*args)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def protected?(*args)
|
|
41
|
-
interpreter.alphabet.include?(args.first.to_s)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def filter_first(*args)
|
|
45
|
-
interpreter.putm(args.first.to_s) ?
|
|
46
|
-
forward(*args) :
|
|
47
|
-
response_to_rule_breaking.call(*args)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def forward(*args)
|
|
51
|
-
@protege.send(*args)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|