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: 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 Structure (Annotated)
Narrative Method Structure
- Gather input
- Perform work
- Deliver results
- 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
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.
- Error
- 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
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
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
- Question!
-
Contact!
- avdi.org / @avdi
- Rate!