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 #=> falseadapted from Confident Ruby, Avdi Grimm, pg. 82-83
Evaluate with ===
even = ->(x){x % 2 == 0}
even === 4 #=> true
even === 5 #=> falseadapted 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: badapted 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_valueadapted 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 definedadapted 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
#=> Eadapted 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 suppliedadapted 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] #=> niladapted from Confident Ruby, Avdi Grimm, pg. 175-177
Empty methods:
def empty
#TODO
end
empty #=> niladapted from Confident Ruby, Avdi Grimm, pg. 175-177
Conditionals with unreached branches:
result = if 2 + 2 == 5
"uh-oh"
end
result #=> niladapted 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 #=> niltype = case :foo
when Integer then 'integer'
when String then 'string'
end
type #=> nil
Integer === :foo
#=> false
Symbol === :foo
#=> trueadapted 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 #=> niladapted from Confident Ruby, Avdi Grimm, pg. 175-177
Unset (misspelled) instance variables:
@i_can_haz_spelling = true
@i_can_haz_speling #=> niladapted from Confident Ruby, Avdi Grimm, pg. 175-177
Some Ruby core methods return nil for 'not found':
[1, 2, 3].detect{|n| n == 5}
#=> niladapted 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