Last active
July 31, 2022 11:57
-
-
Save serradura/173e0dfbc0fd2d7b362ed4f069a59978 to your computer and use it in GitHub Desktop.
mecha.rb - a minimalist finite state machine implemented using Ruby 3.1 (basic = 34 LOC, enhanced = 53 LOC)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Mecha | |
private attr_accessor(:states_map, :callbacks, :transitions, :current_state) | |
public :transitions, :current_state | |
def initialize(initial_state:, transitions:) | |
self.states_map = transitions.parameters.select { |(type, _)| type == :keyreq }.to_h { [_2, _2] } | |
self.callbacks = Hash.new { |hash, key| hash[key] = [] } | |
self.transitions = transitions.call(**states_map).transform_values(&:freeze).freeze | |
self.current_state = states_map.fetch(initial_state) | |
end | |
def trigger?(event) = transitions.fetch(event).key?(current_state) | |
def trigger(event, data: nil) | |
return unless trigger?(event) | |
self.current_state = | |
transitions | |
.dig(event, current_state) | |
.tap { callbacks[_1].each { |callback| callback.call(event, data) } } | |
end | |
def trigger!(event, data = nil) | |
trigger(event, data) or fail ArgumentError.new("Invalid event :#{event} from state :#{current_state}") | |
end | |
def states = states_map.keys | |
def events = transitions.keys | |
def permitted_events = events.select { |event| trigger?(event) } | |
def on(event, &block) = callbacks[event].push(block) && true | |
def off(event) = callbacks.delete(event) && true | |
end | |
# ==================== | |
# == Usage examples == | |
# ==================== | |
machine = Mecha.new( | |
initial_state: :red, | |
transitions: ->(red:, yellow:, green:) { | |
{ | |
ready: { red => yellow }, | |
go: { yellow => green }, | |
stop: { green => red } | |
} | |
} | |
) | |
machine.states # [:red, :yellow, :green] | |
machine.events # [:ready, :go, :stop] | |
machine.current_state # :red | |
machine.permitted_events # [:ready] | |
machine.trigger(:ready) # :yellow | |
machine.current_state # :yellow | |
machine.permitted_events # [:go] | |
machine.trigger(:stop) # nil | |
machine.trigger?(:stop) # false | |
machine.trigger(:go) # :green | |
machine.current_state # :green | |
machine.permitted_events # [:stop] | |
machine.trigger(:stop) # :red | |
machine.current_state # :red | |
machine.on(:yellow) { puts 'The state changed to yellow.' } | |
machine.on(:yellow) { |event, data| puts "Received event: #{event.inspect}, along with data: #{data.inspect}." } | |
machine.trigger(:ready) | |
# The callbacks will print the following messages: | |
# The state changed to yellow. | |
# Received event: :ready, along with data: nil. | |
machine.trigger(:go) | |
machine.trigger(:stop) | |
machine.trigger(:ready, data: '123') | |
# The callbacks will print the following messages: | |
# The state changed to yellow. | |
# Received event: :ready, along with data: "123". | |
machine.off(:yellow) | |
machine.trigger(:go) | |
machine.trigger(:stop) | |
machine.trigger(:ready) | |
# Nothing will be printed | |
machine.trigger(:stop) # false | |
machine.trigger!(:stop) # Invalid event :stop from state :yellow (ArgumentError) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Mecha | |
private attr_accessor(:_states, :restorable, :initial_state, :terminal_state) | |
private attr_accessor(:current_state, :transitions, :callbacks) | |
public :current_state, :transitions | |
def initialize(transitions:, initial_state: nil, terminal_state: nil, restorable: false) | |
self._states = transitions.parameters.select { |(type, _)| type == :keyreq }.to_h { [_2, _2] } | |
self.restorable = restorable | |
self.initial_state = _states.fetch(initial_state&.to_sym, :none) | |
self.terminal_state = _states.fetch(terminal_state&.to_sym, :none) | |
self.current_state = self.initial_state | |
self.transitions = | |
transitions | |
.call(**_states) | |
.transform_values { _1.each_with_object({}) { |(from, to), hash| Array(from).each { |key| hash[key] = to } } } | |
.freeze | |
self.callbacks = Hash.new { |hash, key| hash[key] = [] } | |
end | |
def trigger?(event) = transitions.fetch(event).key?(current_state) | |
def trigger(event, data: nil, callback: true) | |
return unless trigger?(event) | |
self.current_state = | |
transitions | |
.dig(event, current_state) | |
.tap { callbacks[_1].each { |block| block.call(event, data) } if callback } | |
end | |
def trigger!(event, data: nil, callback: true) | |
trigger(event, data:, callback:) or fail ArgumentError.new("Invalid event :#{event} from state :#{current_state}") | |
end | |
def events = transitions.keys | |
def permitted_events = events.select { |event| trigger?(event) } | |
def on(event, &block) = callbacks[event].push(block) && true | |
def off(event) = callbacks.delete(event) && true | |
def is?(state) = state == :none || current_state == _states.fetch(state) | |
def states = _states.keys | |
def terminated? = current_state == terminal_state | |
def restore!(state = nil) | |
new_state = state ? _states.fetch(state) : initial_state | |
restorable && self.current_state = new_state | |
end | |
end | |
# ==================== | |
# == Usage examples == | |
# ==================== | |
machine = Mecha.new( | |
restorable: true, | |
initial_state: :red, | |
terminal_state: :off, | |
transitions: ->(red:, yellow:, green:, off:) { | |
{ | |
ready: { red => yellow }, | |
go: { yellow => green }, | |
stop: { green => red }, | |
turn_off: { [yellow, green] => off } | |
} | |
} | |
) | |
machine.states # [:red, :yellow, :green] | |
machine.events # [:ready, :go, :stop] | |
machine.is?(:red) # true | |
machine.current_state # :red | |
machine.permitted_events # [:ready] | |
machine.trigger(:ready) # :yellow | |
machine.current_state # :yellow | |
machine.permitted_events # [:go] | |
machine.is?(:yellow) # true | |
machine.is?(:red) # false | |
machine.trigger(:stop) # nil | |
machine.trigger?(:stop) # false | |
machine.trigger(:go) # :green | |
machine.current_state # :green | |
machine.permitted_events # [:stop] | |
machine.trigger(:stop) # :red | |
machine.current_state # :red | |
machine.on(:yellow) { puts 'The state changed to yellow.' } | |
machine.on(:yellow) { |event, data| puts "Received event: #{event.inspect}, along with data: #{data.inspect}." } | |
machine.trigger(:ready) | |
# The callbacks will print the following messages: | |
# The state changed to yellow. | |
# Received event: :ready, along with data: nil. | |
machine.trigger(:go) | |
machine.trigger(:stop) | |
machine.trigger(:ready, data: '123') | |
# The callbacks will print the following messages: | |
# The state changed to yellow. | |
# Received event: :ready, along with data: "123". | |
machine.restore!(:red) | |
machine.trigger(:ready, callback: false) | |
# Nothing will be printed | |
machine.off(:yellow) | |
machine.restore!(:red) | |
machine.trigger(:ready, callback: true) | |
# Nothing will be printed | |
machine.trigger(:turn_off) | |
puts '----' | |
puts machine.terminated? | |
machine.restore!(:green) | |
puts '----' | |
machine.trigger!(:turn_off) | |
puts machine.terminated? | |
puts '----' | |
machine.trigger(:stop) # false | |
machine.trigger!(:stop) # Invalid event :stop from state :yellow (ArgumentError) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment