Skip to content

Instantly share code, notes, and snippets.

@bcardiff
Last active August 29, 2015 14:24
Show Gist options
  • Save bcardiff/0ed6a16a86588427fb05 to your computer and use it in GitHub Desktop.
Save bcardiff/0ed6a16a86588427fb05 to your computer and use it in GitHub Desktop.
fsm from a ruby way to a crystal way

This intent to show how Ruby practice can be addapted to Crystal. With a total different under the hood implementation, but keeping some of the greatest aspects.

The focus thou is not to stand on how a fsm should be modeled, or defined by a dsl, BUT how some common Ruby practices might be rethinked to take advantage of Crystal.

As an intermediate step, I sometimes like to think in a more plain type/classes design to have a nice compile time experience, but using macros in crystal to reach that from a nice dsl.

NB: There is consideration to name the examples a language way and not the language way .

module HashFsm
class InvalidState < Exception
end
class InvalidEvent < Exception
end
macro included
@@transitions = {} of Symbol => Hash(Symbol, Symbol)
getter status :: Symbol
def trigger(event)
if trans = @@transitions[event]?
if next_status = trans[@status]?
@status = next_status
else
raise InvalidState.new
end
else
raise InvalidEvent.new
end
end
end
macro initial(status)
@status = {{status}}
end
macro on(event, transitions)
@@transitions[{{event}}] = {{transitions}}
end
end
class Confirmation
include HashFsm
initial :pending
on :confirm, { pending: :confirmed }
on :ignore, { pending: :ignored }
on :reset, { confirmed: :pending, ignored: :pending }
end
fsm = Confirmation.new
pp fsm.status # => :pending
fsm.trigger :confirm
pp fsm.status # => :confirmed
fsm.trigger :reset
pp fsm.status # => :pending
fsm.trigger :ignore
pp fsm.status # => :ignored
# fsm.trigger :confirm # => raise InvalidState
# fsm.trigger :another_event # => raise InvalidEvent
class Confirmation2
class InvalidState < Exception
end
class Pending
def self.name
:pending
end
def self.confirm
Confirmed
end
def self.ignore
Ignored
end
end
class Confirmed
def self.name
:confirmed
end
def self.reset
Pending
end
end
class Ignored
def self.name
:ignored
end
def self.reset
Pending
end
end
def initialize
@status = Pending
end
def status
@status.name
end
def confirm
current = @status
if current.responds_to?(:confirm)
@status = current.confirm
else
raise InvalidState.new
end
end
def ignore
current = @status
if current.responds_to?(:ignore)
@status = current.ignore
else
raise InvalidState.new
end
end
def reset
current = @status
if current.responds_to?(:reset)
@status = current.reset
else
raise InvalidState.new
end
end
end
fsm = Confirmation2.new
pp fsm.status # => :pending
fsm.confirm
pp fsm.status # => :confirmed
fsm.reset
pp fsm.status # => :pending
fsm.ignore
pp fsm.status # => :ignored
# fsm.confirm # => raise InvalidState
# fsm.another_event # undefined method 'another_event' for Confirmation2
module Fsm
class InvalidState < Exception
end
macro decl_state(name)
class {{name.capitalize.id}}
def self.name
{{name}}
end
{{yield}}
end
end
macro initial(status)
decl_state({{status}})
@status = {{status.capitalize.id}}
def status
@status.name
end
end
macro on(event, transitions)
{% for k, v in transitions %}
decl_state({{k}}) do
def self.{{event.id}}
{{v.capitalize.id}}
end
end
{% end %}
def {{event.id}}
current = @status
if current.responds_to?({{event}})
@status = current.{{event.id}}
else
raise InvalidState.new
end
end
end
end
class Confirmation3
include Fsm
initial :pending
on :confirm, { pending: :confirmed }
on :ignore, { pending: :ignored }
on :reset, { confirmed: :pending, ignored: :pending }
end
fsm = Confirmation3.new
pp fsm.status # => :pending
fsm.confirm
pp fsm.status # => :confirmed
fsm.reset
pp fsm.status # => :pending
fsm.ignore
pp fsm.status # => :ignored
# fsm.confirm # => raise InvalidState
# fsm.another_event # undefined method 'another_event' for Confirmation3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment