Confident Code

Avdi Grimm (@avdi)

What this talk is about

  • A style of Code Construction
    code-complete.jpg

What is confident code?

The opposite of timid code. Duh.

Timid Code

First, a story…

Poor Storytelling

  • Tangents
  • Digressions
  • Lack of certainty
  • …code can have these qualities as well.

Timid Code

zoe.jpg

Timid Code

  • Lives in fear
  • Uncertain
  • Prone to digressions
  • Constantly second-guessing itself
  • Randomly mixes input gathering, error handling, and business logic
  • Imposes cognitive load on the reader

Confident Code

sandy.jpg

Confident Code

  • Says exactly what it intends to do
  • No provisos or digressions
  • Has a consistent narrative structure

Source Code

  • cowsay.rb
  • An interface to the "cowsay" program

Cowsay: ASCII art animals

$ echo "Mooo" | cowsay
 ______
< Mooo >
 ------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Cowsay Options

$ echo "Ontogeny Recapitulates Phylogeny"
  | cowsay -e "00"
 __________________________________
< Ontogeny Recapitulates Phylogeny >
 ----------------------------------
        \   ^__^
         \  (00)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Installing Cowsay

  • Debian/Ubuntu
    sudo apt-get install cowsay
    
  • Mac OS X
    sudo port install cowsay
    # or `cauldron stir cowsay`
    # or whatever you Mac kids are using now
    

Cowsay.rb

require 'lib/cowsay'
cow = Cowsay::Cow.new
puts cow.say "Baaa"
# >>  ______
# >> < Baaa >
# >>  ------
# >>         \   ^__^
# >>          \  (oo)\_______
# >>             (__)\       )\/\
# >>                 ||----w |
# >>                 ||     ||

Cowsay.rb

  • Contrived for the sake of example
  • Drawn from real production code crap I've written

Timid Code Structure

timid-code-plain.png

Timid Code Structure (Annotated)

timid-code-annotated.png

The Narrative structure

  1. Gather input
  2. Perform work
  3. Deliver results
  4. handle failure

In that order!

Step 1: Gather Input

  • To be confident, we must be sure of our inputs

On Duck Typing

  • True duck typing is a confident style
  • Duck typing doesn't ask "are you a duck?"
    object.is_a?(String)
    
  • Or even "can you quack?"
    object.respond_to?(:each)
    
  • Treat the object like a duck
    (If it isn't it will complain)
  • Some objects need a little help discovering their duck nature

The Switch (case) Smell

  • Typecasing: Code that switches on the type of an object
  • The opposite of duck typing
  • A reaction to input variance
  • nil checks are the most common typecase
  • It doesn't have to be this way.

Dealing with Uncertain Input

  • Coerce
  • Reject
  • Ignore

When in doubt, coerce

  • Use #to_s, #to_i, Array() liberally
  • Ruby standard libs do this
    require 'pathname'
    path = Pathname("/home/avdi/.emacs")
    open(path) # => #<File:/home/avdi/.emacs>
    

Array()

  • The "arrayification operator"
    Array([1,2,3])                # => [1, 2, 3]
    Array("foo")                  # => ["foo"]
    Array(nil)                    # => []
    
    # Gotcha:
    Array("foo\nbar")             # => ["foo\n", "bar"]
    

Prefer Array() to to_a

Object.new.to_a
# => [#<Object:0xb78a665c>]
# !> default `to_a' will be obsolete

Using Array()

  • Before
    messages = case message
               when Array then message
               when nil then []
               else [message]
               end
    
  • After
    messages = Array(message)
    

Gluing feathers to a pig

  • When there is no consistent interface
  • Decorator Pattern
  • Wraps an object and adds a little extra

Decorator Candidate

destination = case options[:out]
              when nil  then "return value"
              when File then options[:out].path
              else options[:out].inspect # String?
              end
@logger.info "Wrote to #{destination}"

The WithPath Decorator

require 'delegate'
# ...
class WithPath < SimpleDelegator
  def path
    case __getobj__
    when File then super
    when nil then "return value"
    else inspect
    end
  end
end

Using the Decorator

destination = WithPath.new(options[:out]).path
# ...
@logger.info "Wrote to #{destination}"

Other Ways to Massage Objects

  • Dynamically #extend objects
    obj.extend(MyMethods)
    
  • Dynamically add methods
    def obj.foo
       # ...
    end
    

Reject Unexpected Values

  • Not a duck. Not even a bird.
  • Will eventually cause an error
  • …but not before doing some damage
  • May not be clear where the error originated

Assertive Code

  • Confident Code asserts itself
  • Preconditions: state your demands up-front
  • Part of Design by Contract
  • No DbC framework needed
  • Assertions don't have to be spelled "assert()"

Basic Precondition

def say(message, options={})
  if options[:cowfile] && options[:cowfile] =~ /^\s*$/
    raise ArgumentError, "Cowfile cannot be blank"
  end
  # ...
  if options[:cowfile]
    command << " -f #{options[:cowfile]}"
  end
  # ...
end

Assertion Method

def assert(value, message="Assertion failed")
  raise Exception, message, caller unless value
end

# ...

options[:cowfile] and
  assert(options[:cowfile].to_s !~ /^\s*$/)

Assertion Notes

  • Exception skips default rescue
    begin
      raise Exception, "Can't catch me!"
    rescue # rescues StandardError
      puts "Rescued!"
    end
    # ~> -:2: Can't catch me! (Exception)
    
  • Third argument to raise sets stack trace
    raise Exception, message, caller
    

More on Assertions

  • Assertions are documentation

Ignore Unnacceptable Values

  • Guard Clause
  • Special Case
  • Null Object

Guard Clause

  • Short-circuit on nil message
    def say(message, options={})
      return "" if message.nil?
      # ...
    end
    
  • Avoids special cases later in method

Special Case Pattern

  • Some arguments just have to be special
  • An OO way to deal with special cases
  • An object to represent the special case
  • Avoids if, &&, try()

&& and try()

# $? is either nil or a Process::Status object
def check_child_exit_status!(status=$?)
  unless [0,172].include?(status && status.exitstatus)
    raise ArgumentError,
          "Command exited with status "\
          "#{status.try(exitstatus)}"
  end
end

Substitute Special Case for nil

# $? is either nil or a Process::Status object
def check_child_exit_status!(status=$?)
  status ||= OpenStruct.new(:exitstatus => 0)
  unless [0,172].include?(status.exitstatus)
    raise ArgumentError,
          "Command exited with status"\
          "#{status.exitstatus}"
  end
end

Null Object

"A Null Object is an object with defined neutral ("null") behavior." (Wikipedia)

  • The special case value is usually nil
  • The special case for nil is usually "do nothing"
  • A special case of Special Case
  • Responds to any message with nil (or itself)
  • Should be in the standard library

A Basic Null Object

class NullObject
  def method_missing(*args, &block)
    self
  end

  def nil?; true; end
end

def Maybe(value)
  value.nil? ? NullObject.new : value
end

The Black Hole Null Object

  • Returns self from every call
  • Nullifies chains of calls
    foo = nil
    Maybe(foo).bar.baz + buz        # => nil
    

Using the Null Object

  • if statement
    if options[:out]
      options[:out] << output
    end
    
  • Null Object
    Maybe(options[:out]) << output
    

Zero Tolerance for nil

  • nil is overused in Ruby code
  • It means too many things
    • Error
    • Missing Data
    • Flag for "default behavior"
    • Uninitialized variable
    • Default return value for if, unless, empty methods.
  • Nil checks are the most common form of timid code

Where'd that nil come from?!

post_id = params[:post_id]
post    = if params[:blog]
            blog = get_blog(params[:blog])
            blog.posts.find(:first, post_id)
          end
post.date     # =>
# ~> -:4: undefined method `date' 
#    for nil:NilClass (NoMethodError)

Eliminating nil

Kill it with fire.

Use Hash#fetch when appropriate

collection.fetch(key) { fallback_action }

#fetch as an assertion

  • Default error
    opt = {}.fetch(:required_opt)
    # ~> -:1:in `fetch': key not found (IndexError)
    
  • Custom error
    opt = {}.fetch(:required_opt) do
      raise ArgumentError, "Missing option!"
    end
    # ~> -:2: Missing option! (ArgumentError)
    

#fetch for defaults

  • Using ||
    width = options[:width] || 40
    command << " -W #{width}"
    
  • Using #fetch
    width = options.fetch(:width) {40}
    command << " -W #{width}"
    
  • A more explicit default
  • One less conditional

Don't use nil as a default

  • Where did that nil come from?
    @logger = options[:logger]
    # ...
    @logger.info "Something happened" # =>
    # ~> -:3: undefined method `info' for
    #    nil:NilClass (NoMethodError)
    

Symbol Default

@logger = options.fetch(:logger){:no_logger_set}
# ...
@logger.info "Something happened" # =>
# ~> -:3: undefined method `info' for
#    :no_logger_set:Symbol (NoMethodError)

Null Object Default

def initialize(logger=NullObject.new)
  @logger = logger
end

Null Object with Origin

class NullObject
  def initialize
    @origin = caller.first
  end
  # ...
end
nil

Null Object with Origin

foo = NullObject.new
bar = foo.bar
baz = bar.baz
# Now let's see where that nil originated
baz
#<NullObject:0x7fcace309800 @origin="(irb):68:in `new'">

Step 2: Perform Work

  • Keep the focus on the work
  • Avoid digressions for error/input handling

PIE Principle

  • Program Intently and Expressively
    pad.jpg
  • Say what you mean!

Conditionals for Business Logic

  • Conditionals give you more to think about
  • Reserve conditionals for business logic
  • Minimize conditionals for error, input handling
  • Isolate error/input handling from program flow

Conditionals

  • Business logic
    if post.published?
      # ...
    end
    
  • Business logic?
    if post
      # ...
    end
    

Confident Styles of Work

Confident Style: Chaining Work

def slug(text)
  Maybe(text).downcase.strip.tr_s('^a-z0-9', '-')
end

slug("Confident Code") # => "confident-code"
slug(nil)              # => #<NullObject:0xb780863c>

Confident Style: Iteration

  • As exemplified by jQuery
    // From "jQuery in Action"
    $("div.notLongForThisWorld").fadeOut().
        addClass("removed");
    
  • Single object operations are implicitly one-or-error
  • Iteration is implicitly 0-or-more
  • Chains of enumerable operations are self-nullifying
  • Cowsay#say uses an iterative style…

Iterative Style

def say(message, options={})
  # ...
  messages = Array(message)
  message.map { |message|
    # ...
  }
  # ...
end

cow.say([]) # => ""

Step 3: Deliver Results

Don't return nil

  • Help your callers be confident
  • Return a Special Case or Null Object
  • Or raise an error

Step 4: Handle Failure

  • Put the happy path first
  • Put error-handling at the end
  • Or in other methods.

The Ruby Exception Handling Idiom

def foo
  # ...
rescue => error # --- business/failure divider ---
  # ...
end
  • Neatly divides method into "work" and "error handling"

Extract error handling methods

  • Bouncer Method
  • Checked Method

Bouncer Method

  • Method whose job is to raise or do nothing
    def check_child_exit_status
      result = yield
      status = $? || OpenStruct.new(:exitstatus => 0)
      unless [0,172].include?(status.exitstatus)
        raise ArgumentError,
              "Command exited with status"\
              "#{status.exitstatus}"
      end
      result
    end
    

Bouncer Method in Use

# Before:
@io_class.popen(command, "w+") # ...
# ...
if $? && ![0,172].include?($?.exitstatus)
  raise ArgumentError,
        "Command exited with status #{$?.exitstatus.to_s}"
end

# After:
check_child_exit_status {
  @io_class.popen(command, "w+") # ...
}

begin...rescue...end

@io_class.popen(command, "w+") do |process|
  results << begin
               process.write(message)
               process.close_write
               result = process.read
             rescue Errno::EPIPE
               message
             end
end

Checked Method

def checked_popen(command, mode, fail_action)
  check_child_exit_status do
    @io_class.popen(command, mode) do |process|
      yield(process)
    end
  end
rescue Errno::EPIPE
  fail_action.call
end

Checked Method in Use

checked_popen(command, "w+", lambda{message}) do 
  |process|
  # ...
end

The Final Product

confident-code-annotated.png

Observations on the Final Product

  • It has a coherent narrative structure
  • It has lower complexity
  • It's not necessarily shorter

Why Confident Code?

  • Fewer paths == fewer bugs
  • Easier to debug
  • Self-documenting
    "Write code for people first, the computer second"

Review

  • A style, not a set of techniques
  • Consistent narrative structure
  • Handle input, perform work, deliver output, handle failure.
  • PIE: Program Intently and Expressively
  • Avoid digressions
  • State your demands up front
  • Terminate nils with extreme prejudice
  • Isolate error handling from the main flow

Thank You

exceptional-ruby-cover-small.png