A Simple Sandwich, Contorno

A Simple Sandwich, Contorno

Refactoring for a more declarative, functional lunch with friends…

This post is the third course (really more of a side-dish) in a series of silly articles where I use Ruby to build sandwiches. It won’t make much sense if you haven’t read Part I and Part II first. Good luck!

Also, my updated gist is available here if you want to skip straight to dessert.

At the time of writing, I know how to program reasonably well in about 2.001 languages1. I’ve found that using external resources for learning new languages or advancing existing proficiency usually requires finding an author I can sufficiently relate to: I need someone who describes concepts with examples familiar enough that I can map them onto my existing knowledge, because then I can see how what I already know is similar or different. Building that sense of contrast and recognition gradually furnishes a clunky, working familiarity that I ideally build into fluency with practice and subsequent exploration as my confidence grows. At the outset, then, it’s critical for me that I can form that initial bridge; examples that make sense help me learn — surprise.

So, I’m currently enjoying a weird opportunity: my friend and former Lighthouse Labs mentor, Tim Johns, was kind enough to read my silly Sandwich article and got to thinking about how he would have approached the same problem, ultimately writing his own response article. Besides serving as a bonus code review by a deeply respected colleague (and flattering as heck), Tim’s current language of choice is Haskell and programming paradigm is functional — these are far removed from my main experience as an object-oriented Ruby programmer, so they already present a juicy learning opportunity. What makes it a windfall is that Tim is working from my own examples, first replicating (transliterating?) my code into working but non-ideomatic Haskell2, and then re-writing it without the Ruby-accent, free to showcase Haskell’s paradigmatic and philosophical affordances. It’s like he came to the island of Dannyland, tidied up the coastline, built a bridge to Timtopia that lands at faithfully engineered “Little Dannyland” welcome village, and then implemented conveyor belts on the bridge wherever it started to get steep. I believe his intention in all this was just to have fun, but the result suggests to me that he can’t help being a mentor whenever he does anything.

Purity

In our conversations and as a side-effect3 of his article, I’m developing a greater attentiveness to places where I can keep my functions “pure” — meaning, where possible, my functions should return identical results when fed identical inputs (ignoring state) and avoid changing state themselves. Chef#make_me_a_sandwich!’s sandwich-assembly process seemed like a good place to attempt a refactor. As a reminder:

class Chef
  # ...

  def make_me_a_sandwich!(*requested_flavours)
    # ...

    ensure_condiments_are_actually_condiments
    ensure_bread_is_actually_bread
    ensure_knife

    ensure_condiments_are_available(requested_flavours)
    ensure_enough_bread_is_available(requested_flavours)

    sandwich = Sandwich.new

    @knife.clean!

    surface_to_smear = :top
    current_slice = @slices_of_bread.pop

    requested_flavours.each do |flavour|
      condiment = @condiments.find { |jar| jar.contents == flavour}
      condiment.open!

      @knife.load_from!(condiment)

      current_slice.smear!(
        knife: @knife,
        surface: surface_to_smear
      )

      # If ready, add the current slice to the stack and grab another.
      if current_slice.top.smeared?
        sandwich << current_slice
        current_slice = @slices_of_bread.pop
      end

      # Swap the target surface for the next condiment.
      surface_to_smear = (surface_to_smear == :top ? :bottom : :top)
    end

    # Add the final slice
    sandwich << current_slice

    # The magic happens
    sandwich.build!
    sandwich.cut!(@knife)

    if sandwich.ready_to_eat?  # How could it not be? Still.
      puts "Finit.  One #{sandwich.flavours_human_readable} sandwich; bon appétit!"

      return sandwich  # Needlessly explicit "return"; assume to be flourish.
    else
      puts "Hmm.  Something went wrong despite my genius."
    end
  end

One of the reasons Tim likes functional programming is that it encourages you to break down a problem into its components and try to solve them independently]4. Looking at this function as I’ve written it, with all its reliance on mutating local variables (like my surface_to_smear flippable bit) and instance properties (like the contents of @slices_of_bread as I pop slices off the end of it), I can’t help but feel self-conscious. It’s bloated, doing all kinds of things, and managing all kinds of responsibilities. It is egregiously imperative rather than declarative, so the order of its operations is set manually by me (and isn’t particularly resilient to change or shuffling). Even I can see we’re moving away from inherent limitations of OOP and into Plain-Old-Sloppy territory.

Tidying up My Sloppy Joe

Let me see if I can break this problem down. I need to return a valid sandwich containing n requested_flavours. That means:

  • Bottom and top slices have plain bottom and top surfaces respectively
  • Inner surfaces need to be smeared, with additional slices of bread added if enough surfaces are unavailable.
    • Both surfaces of internal slices of bread are valid condiment recipients.
  • A sandwich can contain no plain slices of bread, excepting the top slice.
  • Sandwich must be ready_to_eat, being both build!ed5 and cut!.

The trickiest part of making this sandwich seems to involve getting the bread surfaces smeared in the right order. My original solution involved searching for the right condiment and applying it to the bread in one jumbled swoop, flipping between the top and bottom of each slice while I iterated through my list of user-requested flavours; I think I can start breaking this problem down by dealing with the condiments and the bread separately.

For the condiments, I’ll abstract the search logic out to a private function that incorporates my ensure_condiments... safety checks and returns an array of ready, open jars. Because of those safety functions, I’m guaranteed that any time the method is called with a given array of flavours, it will always return the same array of jars.6

class Chef
  # ...
private
  def opened_condiment_jars_for(requested_flavours)
    # Insurance:
    ensure_condiments_are_actually_condiments
    ensure_condiments_are_available(requested_flavours)

    # Array Building:
    requested_flavours.map do |flavour|
      condiment = @condiments.find { |jar| jar.contents == flavour}
      condiment.open!

      condiment
    end
  end

Regarding the bread, I’ve already figured out how to know the total number of slices I’ll need for any given sandwich ahead of time with the code:

sufficient_slices = 1 + (requested_flavours.count / 2.0).ceil

I’ll formalize that knowledge in Chef as a class method (representing the idea that to be a Chef is to know how many slices are needed for a given sandwich), and then, as with #opened_condiment_jars_for, I’ll just abstract the bread-collection process out to a private method and pop in the safety checks:

class Chef
  def self.number_of_slices_for(number_of_requested_flavours)
    1 + (number_of_requested_flavours / 2.0).ceil
  end

  # ...
private
  # ...

  def plain_slices_for(number_of_requested_flavours)
    ensure_bread_is_actually_bread
    ensure_enough_bread_is_available(number_of_requested_flavours)

    # #slice! will remove the specified range of elements from the host array
    # and return them as a new array.  The "- 1" accounts for Ruby arrays
    # being zero-indexed.
    @slices_of_bread.slice!(
      0..(Chef.number_of_slices_for(number_of_requested_flavours) - 1)
    )
  end
end

Finally, just so I know I’m dealing with a clean, ready knife, I’ll add one more private method to take care of that.

class Chef
  # ...
private
  # ...

  def readied_knife
    ensure_knife

    @knife.clean!

    @knife
  end
end

Now, at the start of the sandwich-assembling process, I’ll run all three of my new methods and wind up with my ready and usable condiments and sandwich bread as two arrays, plus a reliable smearin’ knife:

def make_me_a_sandwich!(*requested_flavours)
  # Courtesy, etc.

  condiments = opened_condiment_jars_for(requested_flavours)
  sandwich_bread = plain_slices_for(requested_flavours.count)
  knife = readied_knife

  # ...
end

Incidentally, my insurance methods are now all covered in the various prep tasks I’ve just created, so I no longer need the checklist section at the start of #make_me_a_sandwich!. This is starting to feel more declarative and less micro-managed.

Another reason I like this approach is that I wind up with a correctly-sized array of sliced bread that I can map! over: the shape of that array is the same as the shape (and medium) of my final sandwich, whereas the list of requested flavours was not. So, hopefully, I can do something like the following code, and be triumphant:

sandwich_bread.map! do
  # something clever
end  #=> smeared_and_sandwichable_bread

I admit it: I am changing state with my Array#slice! and Array#map! methods7. In the end, I’ve decided that I won’t throw out all of OOP’s benefits while trying to clean up my code with some pure functions — that would be heading down the path of transliterating Haskell into Ruby, and I’d meet Tim coming the other way.

It seems to me that given the right number of slices of bread, the desired condiments, and the validation rules for a sandwich, I ought to come out with the same sandwich every time. That sounds like a pure function — I just need to solve the general case for dealing with any given element of the array.

Initially, I’d been trying to figure out a fancy way of determining which side of which slice of bread a given condiment belonged to using even and odd indices of condiments, but in the end I realized I could just explode my bread array into its surfaces and interleave them with the condiments. Array#zip in Ruby is great for this: it allows me to iterate through multiple arrays at once; normally it returns a new array of matched elements, but I can also give it a block and perform an action at each step:

# Basic Usage:
[ 1, 2, 3 ].zip(['A', 'B', 'C' ])  #=> [ [ 1, 'A' ], [ 2, 'B' ], [ 3, 'C' ] ]

# Fun Usage:
[ 1, 2, 3 ].zip ['A', 'B', 'C' ] do | number, letter |
  puts "#{number} and #{letter}"
end  #=>
1 and A
2 and B
3 and C

If I can zip the correct surfaces up to the correct condiments, it’s just a matter of smearing the former with the latter. I’ll skip the first surface to keep the bottom of the sandwich unsmeared (sandwich_bread_surfaces[1..] in Ruby syntax), and then proceed safe in the knowledge that, because I started with the correct amount of bread, I won’t need to worry about running out or the top of my sandwich getting icky.

… At this point, I run into the fact that I’ve only taught Bread how to be smeared, not Bread::Surfaces. Damn. And, in retrospect, it makes more sense that a Knife should to know how to #smear something, rather than Bread. OOPs. I’ll need to fix all that quickly before I continue. (If you don’t want to be distracted by the gory details, just skip ahead.)

Lobotomizing my Bread

Alright, damn it. A band-aid (it seems to me) would be to have Bread pass self to any Surfaces it instantiates. Then, we could ask any given surface to tell its parent bread to “get smeared” (surface.bread.smear!(etc.)). This feels tortuous and again, it really is the knife that does the smearing. Here’s how I’ve rejiggered these classes:

class Knife
  class EmptyKnifeError < StandardError; end
  class InvalidTargetError < StandardError; end

  # ...

  def smear!(bread_surface)
    unless loaded?
      raise EmptyKnifeError, "This knife is too unbesmirched by condiment to smear anything!  Load it first."
    end

    unless bread_surface.is_a?(SliceOfBread::Surface)
      raise InvalidTargetError, "#{bread_surface} isn't the surface of a slice of bread!  Put the knife down!"
    end

    bread_surface.be_smeared!(self)
  end
end

class SliceOfBread::Surface
  class AlreadySmearedError < StandardError; end

  # ...

  def be_smeared!(knife)
    unless knife.is_a?(Knife)
      raise ::Knife::InvalidKnifeError, "That's not hygienic."
    end

    unless plain?
      raise AlreadySmearedError, "This surface was already smeared!"
    end

    unless knife.loaded?
      raise ::Knife::EmptyKnifeError, "This knife is still too clean to smear with.  How did this even happen...?"
    end

    self.contents = knife.contents
    knife.contents = nil
  end
end

This leaves my SliceOfBread class a lot simpler: it’s a container for Surfaces and a tool for reporting on them.

class SliceOfBread
  attr_reader :top
  attr_reader :bottom

  def initialize
    @top = Surface.new
    @bottom = Surface.new
  end

  def plain?
    top.plain? and bottom.plain?
  end

  def smeared?
    top.smeared? or bottom.smeared?
  end
end

Zip It Up!

Okay, geez. Finally.

class Chef
  def make_me_a_sandwich!
    # Courtesy ...
    # ... Tool Preparation ...

    # Explode my bread into surfaces:
    sandwich_bread_surfaces = sandwich_bread.map do |slice|
      [ slice.bottom, slice.top ]
    end.flatten

    # Skipping the bottom surface, apply condiments until we're out of condiments!
    sandwich_bread_surfaces[1..].zip(condiments).each do | surface, condiment |
      break if !condiment

      knife.load_from!(condiment)
      knife.smear!(surface)
    end

    # ...
  end
end

And It Was Good

At this point, I still have my sandwich_bread array, but now it’s all smeared up good. Let’s deem it a sandwich and go home!

class Chef
  def make_me_a_sandwich!
    # Courtesy ...
    # ... Tool Preparation ...
    # ... Smearification ...

    sandwich = Sandwich.new(*sandwich_bread)

    sandwich.build!
    sandwich.cut!(knife)

    if sandwich.ready_to_eat?  # How could it not be?  Still.
      puts "Finit.  One #{sandwich.flavours_human_readable} sandwich; bon appétit!"

      return sandwich  # Needlessly explicit "return"; assume to be flourish.
    else
      puts "Hmm.  Something went wrong despite my genius."
    end
  end

So:

daddy = Chef.new

lunch = daddy.please.make_me_a_sandwich!("Relish", "Marmite", "Nutella™", "Sriracha")
#=>
Looks like we could use a few things from the store...
Bought me some Relish
Bought me some Marmite
Bought me some Nutella™
Bought me some Sriracha
There we go.  That's better.
This isn't enough bread to make your sandwich.  Let me just grab some more.
Yoink!
Yoink!
Yoink!
... There we go.  Pile o' bread!
I'll probably want a knife for this...
... Full tang mokume gane butter knife.  Perfect.
Finit.  One relish, marmite, nutella™, and sriracha sandwich; bon appétit!
pp lunch #=>
#<Sandwich:0x00007f7142866860
 @built=true,
 @cut=true,
 @slices=
  [#<SliceOfBread:0x00007f7142862210
    @bottom=#<SliceOfBread::Surface:0x00007f7142861cc0 @contents=nil>,
    @top=#<SliceOfBread::Surface:0x00007f7142862148 @contents="Relish">>,
   #<SliceOfBread:0x00007f71428619f0
    @bottom=#<SliceOfBread::Surface:0x00007f7142861720 @contents="Marmite">,
    @top=#<SliceOfBread::Surface:0x00007f7142861770 @contents="Nutella™">>,
   #<SliceOfBread:0x00007f7142861630
    @bottom=#<SliceOfBread::Surface:0x00007f7142861518 @contents="Sriracha">,
    @top=#<SliceOfBread::Surface:0x00007f7142861608 @contents=nil>>]>

Lovely. It was a lot of work for exactly the same sandwich, but I think it smells a bit better and all that exercise helped me work up an appetite.

  1. Mostly Ruby, the barest bit of Python, and I’m counting JavaScript and Typescript together as a solid 0.85, on a good day. ↩︎

  2. He describes this as like translating a poem from Portugese to English, word for word, resulting in parsable grammar and syntax but losing poetic flow (and presumably the original language’s particular affordances and nuances of meaning)8↩︎

  3.  ↩︎

  4. Another is that statically-typed functions tend to be self-explanatory: it’s easy to know what a function will return just by looking at the declaration rather than the contents, and you can rest assured that it won’t mutate any of your other data, being “functional” and “pure”. Conversely, Ruby offers the convention of bang-endings on “dangerous” methods (whatever “dangerous” means to that particular programmer — usually “I might throw an exception” or “I might mutate something”, but ultimately, “buyer beware”, “do your homework”, etc. ↩︎

  5. Tim had a reasonable concern about my Sandwich#build! method. When I wrote it, I imagined that the method’s running of checks was akin to a final tidying up of the stack of smeared bread and deeming it fit to be a real sandwich; Tim pointed out that what this really is is a validation pass: a sandwich can be valid or invalid in my sandwich universe, but a sandwich doesn’t really “build” itself. This should’ve been Sandwich#validate!↩︎

  6. I guess this isn’t strictly pure, since state is modified when missing jars are procured and all jars are opened, but at least it’s idempotent! ↩︎

  7. I love that I’m slice!ing my bread array. ↩︎

  8. Considering Tim’s contemplative and heartfelt appreciation of functional programming, I suspect his choice to use an aesthetic domain for his example is personally apropos. ↩︎