Confident Code

Avdi Grimm (@avdi / avdi.org)

This is a talk about…

JOY

Ruby is designed to make programmers happy.

— Yukihiro "Matz" Matsumoto

Joy

3.times do
  puts "Hello, Ruby world!"
end

We begin our programs boldly…

…and then they meet the real world.

Joy?

(options[:greet_count] || 3).times do
  begin
    salutation = 
      begin
        Salutation.find(options[:salutation_id] || 1)
      rescue SalutationServerError
        nil
      end
    if salutation.nil?
      salutation = "Hello"
    end
    puts "#{salutation}, Ruby world!"
  rescue IOError => error
    logger && logger.error "I/O error in greeter"
  end
end

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

A good story, poorly told

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

Confident Code

  • A style of method construction
  • Tells the story well
  • 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:

    brew install cowsay
    
  • Windows
    Sorry :-(

Cowsay.rb

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

Timid Code Structure

timid-code-plain.png

Timid Code Structure (Annotated)

timid-code-annotated.png

Narrative Method 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)

Typecasing

  • Code that switches on type isn't duck typed
  • "The Switch Smell"
  • Case statements on type
  • Checking for method existence
  • NilClass is a type too
  • null checks are the most common kind of typecasing

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"]
    

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
  • Cleanly adapt objects to a common interface

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}"

Encapsulating the Switch

def cowsink(out)
  case out
  when File then out # nothing special
  when nil then NullSink.new
  else GenericSink.new(out)
  end
end

SimpleDelegator

require 'delegate'
class NullSink
  def path; "return value"; end
  def <<(*); end
end

class GenericSink < SimpleDelegator
  def path; "object: #{inspect}"; end
  # All other methods pass through
end

Using the Decorator

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

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
  • State your needs up-front
  • At the edges of your interface
  • Preconditions: 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*$/)

Ignore Unnacceptable Values

lalala_cant_hear_cat.jpg

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 object to represent the special case
  • Avoids if, &&, try()

&& and try()

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

Substitute Special Case for nil

status ||= OpenStruct.new(:exitstatus => 0)
unless [0,172].include?(status.exitstatus)
  raise ArgumentError,
    "Command exited with status "\
    "#{status.exitstatus}"
end

Null Object

  • 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)

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 
    # => #<NullObject:0xb7814bd0>
    

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

Use Hash#fetch when appropriate

More precise handling of method options.

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

Step 2: Perform Work

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

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.

Extract error handling methods

breakglass.jpg

Failure-Checking Digressions

@io_class.popen(command, "w+") # ...
# ...
status = $? || OpenStruct.new(:exitstatus => 0)
unless [0,172].include?(status.exitstatus)
  raise ArgumentError,
        "Command exited with status "\
        "#{status.exitstatus}"
end

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

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

Inline failure handling

@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, "w+") 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"

But most importantly…

JOY

Thank You