Skip to content

Instantly share code, notes, and snippets.

@sergueif
Last active September 2, 2019 23:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sergueif/c0ffb30363f25c48ac3ca30de126cfed to your computer and use it in GitHub Desktop.
Save sergueif/c0ffb30363f25c48ac3ca30de126cfed to your computer and use it in GitHub Desktop.
Genuenly asking: What are some versatile patterns for making imperative code more functional/testable?
# can arbitrary imperative code be chopped up into a loop with a functional step?
# "comm" indicates some communication to an external computer or otherwise side-effecty step
# order of them presumably matters
# code is made up. It's an arbitrary collection of steps and "if" statements
# BUT! it's very similar to actual code I found at the office. :) #java
# Note: I think the following transformation is harder in typed langs.
# I wish not to deal with that at the moment. Will accept any chance no matter how verbose or clunky.
# Note/Constraint: I'm particularly hoping for a tactic that keeps the code in 1-2 modules,
# not spreading the conditional logic across many classes.
# I’m willing to try (even if the result is silly), because if/else statements in the before case
# would usually be unit-tested/exercised with mocks like `when/thenReturn`,
# whereas the extracted logic can become boolean expressions and be tested/exercised with simple `assertEquals`
#before
class Responder
def respond(input)
if (input.foo?)
bar = @peerA.commA(input.foo)
if (bar.isBarEnough?)
baz = @peerB.commB(bar)
if (baz.stuff?)
qux = @peerA.commC()
quz = @peerC.commD(qux)
return [qux, quz]
end
return [baz, nil]
else
quz = @peerC.commD(nil)
return [nil, quz]
end
end
return nil
end
end
#after
class Responder
def respond(input)
situation = Situation.new(input)
# I get the danger of infinite loop here,
# but figure that can be worked around and it's worth it
while !situation.resolved?
next_step = situation.next_step
case next_step.type
when "A":
bar = @peerA.commA(next_step.args)
situation.stepAperformed(bar)
when "B":
baz = @peerB.commB(next_step.args)
situation.stepBperformed(baz)
when "C":
qux = @peerA.commC(next_step.args)
situation.stepCperformed(qux)
when "D":
quz = @peerC.commD(next_step.args)
situation.stepDperformed(qux)
end
end
return situation.final_value
end
# part of the whole point of this is that
# this class is far easier to test than the "before" example
class Situation
def next_step
#...
end
end
end
### Solution 1
# After a conversation, it occured this is a state machine and can be coded like this
def Responder2
def respond(input)
sit = Situation.new(input, @peerA, @peerB, @peerC)
until sit.is_over?
step = sit.next_step
step.execute
end
sit.final_value
end
class Situation < Struct.new(:input, :peerA, :peerB, :peerC)
def is_over?
defined?(@resolution)
end
def final_value
@resolution
end
def step_a_happened(bar); @bar = bar; end
def step_d_happened(quz); @quz = quz; end
def step_b_happened(baz); @baz = baz; end
def step_c_happened(qux); @qux = qux; end
def resolve(value); @resolution = value; end
def next_step
if !input.foo?
return Resolution.new(self, nil)
end
!defined?(@bar) and return StepA.new(self, @peerA, @input.foo)
if @bar.isBarEnough?
!defined?(@baz) and return StepB.new(self, @peerB, @bar)
if @baz.stuff?
!defined?(@qux) and return StepC.new(self, @peerA)
!defined?(@quz) and return StepD.new(self, @peerC, @qux)
return Resolution.new(self, [@qux, @quz])
else
return Resolution.new(self, [@baz, nil])
end
else
!defined?(@quz) and return StepD.new(self, @peerC, nil)
return Resolution.new(self, [nil, quz])
end
end
end
end
class Resolution < Struct.new(:situation, :value)
def execute
@situation.resolve(@value)
end
end
class StepA < Struct.new(:situation, :peer, :input_a)
def execute
bar = @peer.commA(@input_a)
@situation.step_a_happened(bar)
end
end
class StepD < Struct.new(:situation, :peer, :input_d)
def execute
quz = @peer.commD(@input_d)
@situation.step_d_happened(quz)
end
end
class StepB < Struct.new(:situation, :peer, :input_b)
def execute
baz = @peer.commB(@input_b)
@situation.step_b_happened(baz)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment