What this talk is about Incremental
- A style of Code Construction
What this talk is not about Incremental
- New Ruby tricks
- New libraries
- Just a collection of techniques
What is confident code? Incremental
- The opposite of timid code
Timid Code
- Examples, then themes
Source Code
- cowsay.rb
- An interface to the "cowsay" program
- http://github.com/avdi/cowsay
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
Cowsay.rb
require 'lib/cowsay' cow = Cowsay::Cow.new puts cow.say "Baaa" # => nil # >> ______ # >> < Baaa > # >> ------ # >> \ ^__^ # >> \ (oo)\_______ # >> (__)\ )\/\ # >> ||----w | # >> || ||
Cowsay.rb
- Contrived for the sake of example
- Examples drawn from real production code
Checking for nil
if options[:out] options[:out] << output end
&&
command = "cowsay" if options[:strings] && options[:strings][:eyes] command << " -e '#{options[:strings][:eyes]}'" end
try() and friends Incremental
@post.try(:comments).first.try(:author).try(:email)
- AKA
#andand(),#ergo()
- Cleaner, but doesn't get to the root of the problem
- Still interrupting flow to do a nil check
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
case blocks
destination = case options[:out] when nil then "return value" when File then options[:out].path else options[:out].inspect end @logger.info "Wrote to #{destination}"
raise in the middle of a method
if $? && ![0,172].include?($?.exitstatus) raise ArgumentError, "Command exited with status #{$?.exitstatus.to_s}" end
Timid Code
Timid Code Incremental
- Lives in fear
- Constantly second-guessing itself
- Randomly mixes input gathering, error handling, and business logic
- Imposes cognitive load on the reader
Confident Code Incremental
- Says exactly what it intends to do
- Without provisos or digressions
- Without provisos or digressions
- Has a consistent narrative structure
Narrative structure: Four Parts
- Gather input
- Perform work
- Deliver results
- Handle errors
- In that order!
Haphazard structure
Haphazard structure (Annotated)
Step 1: Gather Input
On Duck Typing Incremental
- True duck typing is a confident style
- Duck typing doesn't ask "are you a duck?"
- Or even "can you quack?"
- Treat the object like a duck. If it isn't it will complain
- Sometimes an object just needs help discovering its duck nature
Dealing with Unnacceptable Input
- Coerce
- Reject
- Ignore
Coerce Values into What You Want Incremental
#to_s,#to_i,#to_sym,Array()
- Decorators
When in doubt, coerce Incremental
- 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>
- Prefer
Array()toto_a
Object.new.to_a # => [#<Object:0xb78a665c>] # !> default `to_a' will be obsolete
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() Incremental
- Before
messages = case message when Array then message when nil then [] else [message] end
- After
messages = Array(message)
Gluing feathers to a pig Incremental
- When there is no consistent interface
- Decorator Pattern
- Wraps an object and adds a little extra
- The Candidate
destination = case options[:out] when nil then "return value" when File then options[:out].path else options[:out].inspect 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 Incremental
- Dynamically
#extendobjects
obj.extend(MyMethods) - Dynamically add methods
def obj.foo # ... end
Reject Unexpected Values Incremental
- Confident Code is Assertive
- Preconditions: state your demands up-front
- Part of Design by Contract
- A complement to TDD/BDD
- 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*$/)
More on Assertions
- Assertions are documentation
- FailFast
http://fail-fast.rubyforge.org/
Ignore Unnacceptable Values
- Guard Clause
- Null Object
Guard Clause Incremental
- Short-circuit on nil message
def say(message, options={}) return "" if message.nil? # ...
- Avoids special cases later in method
- Return from beginning or end of method
Null Object Incremental
"A Null Object is an object with defined neutral ("null") behavior." (Wikipedia)
- 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
Using the Null Object Incremental
ifstatement
if options[:out] options[:out] << output end
- Null Object
Maybe(options[:out]) << output
More on Null Objects
- Returning
selflets us nullify chains of calls
Example coming up later.
Zero Tolerance for nil Incremental
- nil is overused in Ruby code
- It means too many things
- Error
- Missing Data
- Flag for "default behavior"
- Uninitialized variable
- Error
- Nils propagate
- Nil checks are the most common form of timid code
Where'd that nil come from?!
post_id = params[:post_id] post = Post.find(post_id.to_i) post.date # => # ~> -:4: undefined method `date' for nil:NilClass (NoMethodError)
Eliminating nil
Use Hash#fetch when appropriate Incremental
{}[:width] # => nil
{}[:width] || 40 # => 40
{:width => nil}[:width] || 40 # => 40
{:width => false}[:width] || 40 # => 40
{:width => 29}[:width] || 40 # => 29
{}.fetch(:width) # =>
# ~> -:7:in `fetch': key not found (IndexError)
{}.fetch(:width) { 40 } # => 40
{:width => nil}.fetch(:width) {40} # => nil
{:width => false}.fetch(:width) {40} # => false
{:width => 29}.fetch(:width) {40} # => 29
#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 Incremental
- Where did that
nilcome from?
@logger.info "Something happened" # => # ~> -:3: undefined method `info' for nil:NilClass (NoMethodError)
Symbol Default
@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
Stub Object Default
# $? 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.status}" end end
Step 2: Perform Work Incremental
- PIE Principle
Program Intently and Expressively
- Avoid digressions for error/input handling
Conditionals for Business Logic Incremental
- Programming without IF
http://programmingwithoutifs.blogspot.com/ - Conditionals are essential
- Reserve conditionals for business logic
- Minimize conditionals for error, input handling
- Isolate error/input handling from program flow
Business Logic Conditional
if post.published? # ... end
Non-Business Logic Conditional
if post # ... end
Chaining Work
def slug(text) Maybe(text).downcase.strip.tr_s('^a-z0-9', '-') end slug("Confident Code") # => "confident-code" slug(nil) # => #<NullObject:0xb780863c>
Iteration over singular objects Incremental
- A style 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#sayuses an iterative style…
Iterative Style
def say(message, options={}) # ... messages = Array(message) message.map { |message| # ... } # ... end cow.say(nil) # => ""
Iterative Style Tips
- Use *args
def say(*messages) # ...
- Use #compact to get rid of nils
msgs = ["moo", nil, "belch"] cow.say(*msgs.compact)
Step 3: Deliver Results Incremental
- Moving on…
Step 4: Handle Errors Incremental
- Put the happy path first
- Put error-handling at the end
- Or in other methods.
The Ruby Exception Handling Idiom
def foo # ... rescue => error # ... end
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+") # ... }
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
# Before: @io_class.popen(command, "w+") do |process| results << begin # ... rescue Errno::EPIPE message end end # After: checked_popen(command, "w+", lambda{message}) do |process| # ... end
The Final Product
Observations on the Final Product Incremental
- It has a coherent narrative structure
- It has lower complexity
- It's not necessarily shorter
Why Confident Code? Incremental
- Fewer paths == fewer bugs
- Easier to debug
- Self-documenting
"Write code for people first, the computer second"
Review Incremental
- A style, not a set of techniques
- PIE: Program Intently and Expressively
- Consistent Narrative structure
- Handle input, perform work, deliver output, handle errors.
- State your demands up-front
- Terminate nils with extreme prejudice
- Avoid digressions
- Isolate error handling from the main flow
Shameless Plug
- Caliper
http://getcaliper.com/ - Caliper for the Cowsay project
http://getcaliper.com/caliper/project?repo=git://github.com/avdi/cowsay.git
Flog Complexity
Metrics Diff