Let's write some weird Ruby
David Copeland / @davetron5000
The Billion Dollar Mistake
I was designing the first comprehensive type system for references in an object oriented language…I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement.
This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
No More nil
- all variables must have a value
- no default value
- no global symbol for "nothing"
class Person
attr_accessor :name,
:birthdate,
:title
def initialize(name,birthdate,title=nil)
@name = name
@birthdate = birthdate
@title = title
end
def greeting
if title.nil?
"Hello #{@name}"
else
"Hello #{@title} #{@name}"
end
end
end
class Person
attr_accessor :name,
:birthdate,
:title
def initialize(name,birthdate,title=nil)
@name = name
@birthdate = birthdate
@title = title
end
def greeting
if title.nil?
"Hello #{@name}"
else
"Hello #{@title} #{@name}"
end
end
end
$nil = Object.new do
def nil?
true
end
end
$nil = Object.new do
def nil?
true
end
end
nil
- Variable unset
- Don't know value
- There is no value
- Value is “empty”
class Unassigned; end
class Unknown; end
class NoValue; end
class Empty; end
class Person
attr_accessor :name, :birthdate, :title
def initialize(name, birthdate, title=NoValue)
@name = name
@birthdate = birthdate
@title = title
end
end
class Person
attr_accessor :name, :birthdate, :title
def initialize(name, birthdate, title=Unknown)
@name = name
@birthdate = birthdate
@title = title
end
end
class Person
def greet
if title == Unknown ||
title == NoValue ||
title == Unassigned ||
title == Empty
"Hello #{name}"
else
"Hello #{title} #{name}"
end
end
end
class Person
def greet
title.when_value {
"Hello #{title} #{name}"
}.or_else {
"Hello #{name}"
}
end
end
class BasicObject
def when_value(&block)
block.call(self)
end
def or_else(*)
self
end
end
class NilLikeSentinel
def self.when_value(*)
self
end
def self.or_else(&block)
block.call
end
end
Unassigned = Class.new(NilLikeSentinel)
Unknown = Class.new(NilLikeSentinel)
NoValue = Class.new(NilLikeSentinel)
Empty = Class.new(NilLikeSentinel)
class Person
def greet
title.when_value {
"Hello #{title} #{name}"
}.or_else {
"Hello #{name}"
}
end
end
class Person
def greet
title.when_value {
"Hello #{title} #{name}"
}.or_else {
if title == NoValue || title == Empty
"Hello #{name}"
else
"Not sure how to greet you, #{name}"
end
}
end
end
class BasicObject
def when_known(&block)
when_value(&block)
end
end
class NilLikeSentinel
def self.when_known(*)
self
end
end
class KnownNilLikeSentinel < NilLikeSentinel
def self.when_known(&block)
block.call(self)
end
end
NoValue = Class.new(KnownNilLikeSentinel)
Empty = Class.new(KnownNilLikeSentinel)
class Person
def greet
title.when_value {
"Hello #{title} #{name}"
}.when_known {
"Hello #{name}"
}.or_else {
"Not sure how to greet you, #{name}"
}
end
end
class PersonGreeter
def greet(person)
if person.first_name.present?
"Hi, #{person.first_name}"
elsif person.last_name.present?
if person.gender.present? && person.gender.salutation.present?
"Hello #{person.gender.salutation} #{person.last_name}"
else
"Hello #{person.last_name}"
end
else
"Hello"
end
end
end
class PersonGreeterAttributes
def greet(person)
person.with_attributes { |first_name|
"Hi, #{first_name}"
}.else_with_attributes { |gender_salutation,last_name|
"Hello #{gender_salutation} #{last_name}"
}.else_with_attributes { |last_name|
"Hello #{last_name}"
}.or_else {
"Hello"
}
end
end
class AddressNormalizer
def normalize_address(person)
person.with_attributes { |city, zip_code|
person.city = city.strip.downcase
person.zip_code = zipcode.strip.downcase
}
end
end
class AddressNormalizer
def normalize_address(person)
person.with_attributes { |city, zip_code|
person.update_attributes do |update|
update.city.to city.strip.downcase
update.zip_code.to zip_code.strip.downcase
end
}
end
end
class Person
def update_attributes(&block)
update = Update.new
block.(update)
update.each_attribute do |name,new_value|
instance_variable_set("@#{name}",new_value)
end
end
end
class Person
def update_attributes(&block)
updater = Updater.new
block.call(updater)
transaction do
updater.changed_attributes { |attribute,new_value|
self.instance_variable_set("@#{attribute}",value)
}
end
end
end
class Person
def update_attributes(&block)
updater = Updater.new
block.call(updater)
updater.when_updated do |city,zip_code|
unless zip_and_city_consistent?(updater.city,updater.zip_code)
raise "address not consistent"
end
end
updater.changed_attributes { |attribute,new_value|
self.instance_variable_set("@#{attribute}",value)
}
end
end
class Person
def update_attributes(&block)
updater = Updater.new
block.call(updater)
updater.when_updated { |city,zip_code|
unless zip_and_city_consistent?(updater.city,updater.zip_code)
raise "address not consistent"
end
}.else_when_updated { |city|
raise "you cannot update the city without also updating the zipcode"
}
updater.changed_attributes { |attribute,new_value|
self.instance_variable_set("@#{attribute}",value)
}
end
end
class CreditCardCharger
def charge_customer(customer,amount)
result = CreditCardService.charge!(customer,amount)
unless result.success?
if result.error_code == CreditCardService::EXPIRED
"Your card has expired-update and try again"
else
"Sorry, your card was declined"
end
end
rescue => ex
ex.message
end
end
class CreditCardChargerWeird
def charge_customer(customer,amount)
CreditCardServiceWeird.charge!(customer,amount) do |outcome|
outcome.on_success {
# ok!
}.on_decline {
"Sorry, your card was declined"
}.on_expiration {
"Your card has expired-update and try again"
}.on_exception { |ex|
ex.message
}
end
end
end
class CreditCardChargerWeird
def charge_customer(customer,amount)
CreditCardServiceWeird.charge!(customer,amount) do |outcome|
outcome.on_success {
# ok!
}.on_decline {
"Sorry, your card was declined"
}.on_expiration {
"Your card has expired-update and try again"
}.on_exception { |ex|
ex.message
}
end
end
end
class CreditCardChargerMistake
def charge_customer(customer,amount)
CreditCardServiceStrict.charge!(customer,amount) do |outcome|
outcome.on_success {
# ok!
}.on_decline {
"Sorry, your card was declined"
}.on_exception { |ex|
ex.message
}
end
end
end
class CreditCardServiceStrict
def self.charge!(customer,amount,&block)
outcome = Outcome.new
block.call(outcome)
outcome.validate_all_outcomes_handled!
# ...
end
end
class CreditCardServiceStrict
def self.charge!(customer,amount,&block)
outcome = Outcome.new
block.call(outcome)
outcome.validate_all_outcomes_handled!
outcome.handle(
rest_call(make_url(customer,amount))
)
private
def rest_call(_)
# ...
end
def make_url(customer,amount)
# ...
end
end
end
class OutcomeNoIf < Outcome
def handle(result)
status = result.status
method_name = what_to_call(status)
self.send(method_name)
end
end
class OutcomeNoIf < Outcome
private
def what_to_call(status)
if status == 200
:call_on_success
elsif status < 500
:call_on_decline
else
:call_on_exception
end
end
end
class OutcomeNoIf < Outcome
private
def what_to_call(status)
if ( status == 200 ) :call_on_success
if ( status < 500 ) :call_on_decline
else :call_on_exception
end
end
class OutcomeNoIf < Outcome
private
def what_to_call(status)
-> (status) { status == 200 }, :call_on_success
-> (status) { status < 500 }, :call_on_decline
else :call_on_exception
end
end
class OutcomeNoIf < Outcome
private
def what_to_call(status)
[
[ -> (status) { status == 200 }, :call_on_success ],
[ -> (status) { status < 500 }, :call_on_decline ],
]
else :call_on_exception
end
end
class OutcomeNoIf < Outcome
private
def what_to_call(status)
[
[ -> (status) { status == 200 }, :call_on_success ],
[ -> (status) { status < 500 }, :call_on_decline ],
].detect { |predicate,_|
predicate.(status)
}
else :call_on_exception
end
end
class OutcomeNoIf < Outcome
private
def what_to_call(status)
_,method = [
[ ->(status) { status == 200 }, :call_on_success ],
[ ->(status) { status < 500 }, :call_on_decline ],
].detect { |predicate,_|
predicate.(status)
}
method || :call_on_exception
end
end
class Outcome
def handle(result)
status = result.status
method_name = if status < 500
:call_on_decline
elsif status == 200
:call_on_success
else
:call_on_exception
end
self.send(method_name)
end
end
class OutcomeNoIf < Outcome
private
def what_to_call(status)
_,method = [
[ ->(status) { status < 500 }, :call_on_decline ],
[ ->(status) { status == 200 }, :call_on_success ],
].detect { |predicate,_|
predicate.(status)
}
method || :call_on_exception
end
end
class OutcomeNoIf < Outcome
private
def what_to_call(status)
logic = Logic.new
logic.on { |status| status < 500 }.then_return(:call_on_decline)
logic.on { |status| status == 200 }.then_return(:call_on_success)
logic.evaluate!(status, or_else: :call_on_exception)
end
end
class OutcomeNoIf < Outcome
private
def what_to_call(status)
logic = Logic.new
logic.on { |status| status < 500 }.then_return(:call_on_decline)
logic.on { |status| status == 200 }.then_return(:call_on_success)
logic.evaluate!(status, or_else: :call_on_exception)
# => BOOM: NonExclusiveDisjunctionError
end
end
class OutcomeNoIf < Outcome
private
def what_to_call(status)
logic = Logic.new
logic.on { |status| status == 200 }.then_return(:call_on_success)
logic.on { |status| status != 200 &&
status < 500 }.then_return(:call_on_decline)
logic.evaluate!(status, or_else: :call_on_exception)
# => All good!
end
end
class OutcomeNoIf < Outcome
private
def what_to_call(status)
logic = Logic.new
logic.on(&Success).then_return(:call_on_success)
logic.on(&Decline).then_return(:call_on_decline)
logic.evaluate!(status, or_else: :call_on_exception)
end
Success = ->(status) { status == 200 }
Decline = ->(status) { !Success.(status) && status < 500 }
end