Code examples

UMass Transit IT code review, February 17th, 2015

 

code adapted from Confident Ruby by Avdi Grimm

presentation by David Faulkenberry

Feel free to follow along at:

https://slides.com/davidfaulkenberry/deck/live

Levels of confidence

  • Lambdas as case conditions:
        confidence in the language
  • Hash#fetch:
        confidence in accepting input
  • The trouble with nil:
        confidence in your own code

adapted from Confident Ruby, Avdi Grimm, pg. 82-83

Lambdas as case conditions

adapted from Confident Ruby, Avdi Grimm, pg. 82-83

Define a lambda:

even = ->(x){x % 2 == 0}

adapted from Confident Ruby, Avdi Grimm, pg. 82-83

Evaluate with #call

even = ->(x){x % 2 == 0}

even.call 4 #=> true
even.call 5 #=> false

adapted from Confident Ruby, Avdi Grimm, pg. 82-83

Evaluate with ===

even = ->(x){x % 2 == 0}

even === 4 #=> true
even === 5 #=> false

adapted from Confident Ruby, Avdi Grimm, pg. 82-83

Case statements also evaluate with ===

even = ->(x){x % 2 == 0}

def describe(number)
  case number
  when 42   #42 === number
    "the ultimate answer"
  when even #even === number
    "even"
  else
    "odd"
  end
end
 
describe 4  #=> "even"
describe 5  #=> "odd"
describe 42 #=> "the ultimate answer"
Hash#fetch

adapted from Confident Ruby, Avdi Grimm, pg. 118-128

adapted from Confident Ruby, Avdi Grimm, pg. 118-128

Define a hash:

# Ruby < 1.9
h = {:a => 123}

# Ruby >= 1.9
h = {a: 123}

adapted from Confident Ruby, Avdi Grimm, pg. 118-128

The difference between [] and fetch:

h = {a: 123}

#existing keys are handled the same:
h[:a]       #=> 123
h.fetch :a  #=> 123
 

#missing keys are handled differently:
h[:b]       #=> nil
h.fetch :b  #=> KeyError: key not found: b

adapted from Confident Ruby, Avdi Grimm, pg. 118-128

Handle a missing key... raise an exception:

h = {a: 123}

h.fetch :c do
  raise KeyError, "Hey! I couldn't find c!"
end
#=> KeyError: Hey! I couldn't find c!

adapted from Confident Ruby, Avdi Grimm, pg. 118-128

Handle a missing key... use a default:

h = {a: 123}

h.fetch :d do
  :default_value
end
#=> :default_value

adapted from Confident Ruby, Avdi Grimm, pg. 118-128

Side note: this can be given in non-block syntax...

h = {a: 123}

h.fetch :d, :default_value
#=> :default_value

...but this will always execute:

h = {a: 123}

h.fetch :a, some_external_api_call
#=> 123
#but also executes external api call


h.fetch :a { some_external_api_call }
#=> 123
#doesn't execute api call, since h[:a] is defined

adapted from Confident Ruby, Avdi Grimm, pg. 118-128

Handle a missing key... operate on the key:

h = {a: 123}

h.fetch 'e' do |key|
  key.upcase
end
#=> "E"

h.fetch 'e', &:upcase
#=> E

adapted from Confident Ruby, Avdi Grimm, pg. 118-128

Useful if hash is unknown user input:

h = {a: 123}

configurable_mandatory_hash_keys = [:f, :g]

configurable_mandatory_hash_keys.each do |key|
  h.fetch key do |key|
    raise KeyError, "You need to specify #{key}."
  end
end
#=> KeyError, "You need to specify f."

adapted from Confident Ruby, Avdi Grimm, pg. 118-128

User API - guest user, password 'false'

#create guest user
add_user(login: 'guest user', password: false)
#=> Creating guest user...

adapted from Confident Ruby, Avdi Grimm, pg. 118-128

Unconfidently (and incorrectly):

def add_user(attributes)
  login = attributes[:login]
  unless login
    raise ArgumentError, 'Login must be supplied'
  end
  password = attributes[:password]
  unless password
    raise ArgumentError, 'Password must be supplied'
  end
  if password == false
    puts 'Creating guest user...'
  end
  #...
end
 

add_user(login: 'guest user', password: false)
#=> ?
def add_user(attributes)
  login = attributes[:login]
  unless login
    raise ArgumentError, 'Login must be supplied'
  end
  password = attributes[:password]
  unless password
    raise ArgumentError, 'Password must be supplied'
  end
  if password == false
    puts 'Creating guest user...'
  end
  #...
end
 

add_user(login: 'guest user', password: false)
#=> ArgumentError: Password must be supplied

adapted from Confident Ruby, Avdi Grimm, pg. 118-128

Confidently, using fetch:

def add_user(attributes)
  login = attributes.fetch :login do
    raise ArgumentError, 'Login must be supplied'
  end
  password = attributes.fetch :password do
    raise ArgumentError, 'Password must be supplied'
  end
  if password == false
    puts 'Creating guest user...'
  end
  #...
end
 



add_user(login: 'guest user', password: false)
#=> Creating guest user...

adapted from Confident Ruby, Avdi Grimm, pg. 118-128

Confidently, concisely, and configurably:

MISSING_ATTRIBUTE_ERROR =
  ->(key){raise ArgumentError,
          "#{key.capitalize} must be supplied"}
 
def add_user attributes
  login    = attributes.fetch :login,
             &MISSING_ATTRIBUTE_ERROR
  password = attributes.fetch :password,
             &MISSING_ATTRIBUTE_ERROR
  #...
end
 
add_user(login: 'guest_user', password: false)
#=> Creating guest user...

#with typo...
add_user(loginth: 'guest_user', password: false)
#=> ArgumentError: "Login must be supplied"

adapted from Confident Ruby, Avdi Grimm, pg. 175-177

The trouble with nil

adapted from Confident Ruby, Avdi Grimm, pg. 175-177

Hashes:

h = {}
h[:fnord] #=> nil

#doesn't mean the key doesn't exist:
h = {fnord: nil}
h[:fnord] #=> nil

adapted from Confident Ruby, Avdi Grimm, pg. 175-177

Empty methods:

def empty
  #TODO
end

empty #=> nil

adapted from Confident Ruby, Avdi Grimm, pg. 175-177

Conditionals with unreached branches:

result = if 2 + 2 == 5
    "uh-oh"
  end

result #=> nil

adapted from Confident Ruby, Avdi Grimm, pg. 175-177

Case statements with unreached branches:

type = case :foo
       when Integer then 'integer'
       when String  then 'string'
       end

type #=> nil
type = case :foo
       when Integer then 'integer'
       when String  then 'string'
       end

type #=> nil

Integer === :foo
#=> false
Symbol  === :foo
#=> true

adapted from Confident Ruby, Avdi Grimm, pg. 175-177

... even local variables in an unreached branch:

if 2 + 2 == 5
  tip = 'Follow the white rabbit'
end

tip #=> nil

adapted from Confident Ruby, Avdi Grimm, pg. 175-177

Unset (misspelled) instance variables:

@i_can_haz_spelling = true

@i_can_haz_speling #=> nil

adapted from Confident Ruby, Avdi Grimm, pg. 175-177

Some Ruby core methods return nil for 'not found':

[1, 2, 3].detect{|n| n == 5}
#=> nil

adapted from Confident Ruby, Avdi Grimm, pg. 175-177

A real-life example of 'nil pervasion':

require 'yaml'
SECRETS = File.exist?('secrets.yml')
  && YAML.load_file('secrets.yml')
 
def get_password_for_user(username = ENV['user'])
  secrets = SECRETS || @secrets
  entry = secrets &&
    secrets.detect{|entry| entry['user'] == username}
  entry && entry['password']
end
 
get_password_for_user #=> nil

#How many different potential nils can you spot?

Summary

"Nil is the worst possible representation of a failure: it carries no meaning but can still break things. An exception is more meaningful, but some failure cases aren't really exceptional. When a return value is used but non-essential, a workable but semantically blank object - such as an empty string - may be the most appropriate result."

Confident Ruby, Avdi Grimm, pg. 227

Code examples - 2015-02-17

By David Faulkenberry

Code examples - 2015-02-17

UMass Transit IT code review, February 17th, 2015

  • 893