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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b7874ce91e148aa1ee25266790efe1bbcd41a200c03a57af8ad1c73f350ffc0
4
- data.tar.gz: 5df6382f80e74bd23374f1d22d1a2e67544534ea682563ab89fe4488d042a153
3
+ metadata.gz: 7f7daefff33b5cf15d86669bbaff8c8a8507d54259ac1b4199a8f173aef7bd38
4
+ data.tar.gz: 2d3cf62ea14e5ef8d3164ff5bbeeb84f415c806bdf7f1da63990a1fa53da4553
5
5
  SHA512:
6
- metadata.gz: 27ef54b9755d39c18c51d13b7cd5be867a6fbc31a0ee7fcea4b8598ee98cf29c67fccc1b144001ecb9be577305100cac7c17dd7ce19849dd490e6a2b78658652
7
- data.tar.gz: bb90b7506b1dd352cafcd6f52315d63668eb9c5aeba386e463203aeb1fd632c1617e9a53e2d6990c5b029e17d7eca8cd5d14abe4b65e45a7bb6f0ab338ef74ab
6
+ metadata.gz: d7b95ea00e217342548717c61b75909c63e610110a2597f1d0b78c78b25c171d893d9a9b743799a206a78cdeb748df25d1f636892e59fe570d62040de7695928
7
+ data.tar.gz: c54777928180b9365eabef5ef140b75d67cbd5401483a046862b31a339f588a90a9894e6cdcbf4af5d8364b9dea9455fa18b929303ae2ed3727835166c6470cb
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- moory (0.6.0)
4
+ moory (1.0.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
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
- ![ab_star](images/ab_star.png)
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
- ![ab_star](images/foo_bar_etiquette.png)
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
- ab_star = Moory::Acceptor.create(
3
+ rcg = Moory::Recogniser.new(
4
4
  initial: '0',
5
- transitions: %q{
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: %w{ 1 }
13
+ final: [
14
+ '1'
15
+ ]
14
16
  )
15
17
 
16
-
17
- ab_star.accepts?(string: "abb")
18
- # => true
19
- ab_star.accepts?(string:"abb", in_state: '1')
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
@@ -0,0 +1,16 @@
1
+ require 'moory'
2
+
3
+ decoder = Moory::Decoder.new(
4
+ rules: %q{
5
+ 0 : a / a : 1
6
+ 0 : b / b : 2
7
+ 1 : a : 1
8
+ 1 : b / b : 2
9
+ 2 : a / a : 1
10
+ 2 : b : 2
11
+ },
12
+ initial: '0'
13
+ )
14
+
15
+ decoder.decode("abababababbabbabaa")
16
+ # => abababababababa
data/lib/moory.rb CHANGED
@@ -1,9 +1,14 @@
1
1
  require "moory/version"
2
- require "moory/interpreter"
3
- require "moory/parser"
4
- require "moory/acceptor"
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
- module Decoder
3
- def Decoder.create(config)
4
- Interpreter.new do |i|
5
- i.load(config[:transitions])
6
-
7
- define_singleton_method(:decode) do |string|
8
- i.state = config[:initial]
9
- i.fallback_effector = config
10
- .fetch(:ostream, $stdout)
11
- .method(:write)
12
-
13
- string.each_char do |c|
14
- i.putm(c)
15
- end
16
- end
17
- end
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
@@ -0,0 +1,11 @@
1
+ module Moory
2
+ module Loader
3
+ def Loader.load(rules:, machine:)
4
+ prs = Moory::RuleParser::FileReader.new
5
+ ary = prs.analyse(rules)
6
+ ary.each do |params|
7
+ machine.transitions.store(params)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Moory
2
- VERSION = "0.6.0"
2
+ VERSION = "1.0.0"
3
3
  end
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.6.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 00:00:00.000000000 Z
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/readme_example.rb
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/interpreter.rb
76
+ - lib/moory/loader.rb
77
+ - lib/moory/machine.rb
84
78
  - lib/moory/pair.rb
85
- - lib/moory/parser.rb
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:
@@ -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
@@ -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
@@ -1,12 +0,0 @@
1
- module Moory
2
- Arrow = Struct.new(
3
- :source,
4
- :label,
5
- :target,
6
- keyword_init: true
7
- ) do
8
- def valid?
9
- source && label && target
10
- end
11
- end
12
- end
@@ -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
@@ -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