I work at Braintree Payments. We make it super easy for businesses of any size to take credit
card payments online.
Slide 3
Slide 3 text
We handle payments for thousand of really awesome companies (like these).
Slide 4
Slide 4 text
Immutability
Immutability, simply put, means that something can't change. In Ruby, an immutable object
would be one that can't me modified after it's created.
Obviously, a program that doesn't change anything isn't particularly useful, but in small
pockets, immutability can be a tool for helping you reason about and structure your code.
I'm going to tell you some stories of bad code (full of mutable state) that I've written that
came back to bite me, and some stories of good code that I've written in an immutable style
and how it paid off.
Slide 5
Slide 5 text
Your code is full
of immutability.
Make it explicit.
Your programs already contain lots of data that you assume will never change, and I'd
encourage you to make it explicit.
Slide 6
Slide 6 text
class Purchase < ActiveRecord::Base
# t.integer :user_id
# t.integer :price
# t.integer :item_id
end
Let's pretend you're running an online store, and you record all of your purchases in a
purchase table.
You probably never want to update the data in this table. If you were to come in and change
the value of the "price" column, it wouldn't actually change how much you charged the users
credit card.
When you're recording data that reflects events that have happened outside of your
application (in a 3rd party or in the "real world"), you often want this data to be immutable.
Slide 7
Slide 7 text
class Purchase < ActiveRecord::Base
include Immutable
end
It would be nice if we could mix in a module that makes this model immutable.
Slide 8
Slide 8 text
class Purchase < ActiveRecord::Base
include Immutable
end
module Immutable
def readonly?
persisted?
end
end
Luckily, we can make an Immutable module in 5 lines of code.
Slide 9
Slide 9 text
class Purchase < ActiveRecord::Base
include Immutable
end
module Immutable
def readonly?
persisted?
end
end
purchase = Purchase.create(...)
purchase.update_attributes(...) #=>
ActiveRecord::ReadOnlyRecord
And now we can create new records, but can't update or delete existing ones.
Slide 10
Slide 10 text
REVOKE UPDATE ON purchases FROM app;
REVOKE DELETE ON purchases FROM app;
You could even go as far as giving your application a special user in the database and
removing the ability to modify or delete your immutable tables at the database level.
Slide 11
Slide 11 text
class Purchase < ActiveRecord::Base
# t.integer :user_id
# t.integer :price
# t.integer :item_id
# t.string :status
end
Here, we've added a status field that will contain the state of the purchase (processing,
shipped, refunded, etc.) and will change, unlike the other fields.
Slide 12
Slide 12 text
github.com/JackDanger/
immutable_attributes
gem install immutable_attributes
But what if you only want to make certain fields immutable? There's a gem for that.
Slide 13
Slide 13 text
class Purchase < ActiveRecord::Base
# t.integer :user_id
# t.integer :price
# t.integer :item_id
# t.string :status
attr_immutable :user_id
attr_immutable :price
attr_immutable :item_id
end
We can call `attr_immutable` for the fields...
Slide 14
Slide 14 text
class Purchase < ActiveRecord::Base
# t.integer :user_id
# t.integer :price
# t.integer :item_id
# t.string :status
attr_immutable :user_id
attr_immutable :price
attr_immutable :item_id
end
purchase.update_attributes(:price => "9.99")
#=> ImmutableAttributeError
...and when we try to modify them, we'll get an error.
Slide 15
Slide 15 text
In your
application too!
You application code also has a bunch of data that you don't really want to change.
Slide 16
Slide 16 text
def build_url(base, path, params={})
base << "/" unless path.start_with?('/')
base << path
base << "?" + params.to_param if params.present?
base
end
Let's pretend you have a `build_url` method that takes a domain name, a path, and some
optional query params, and will build a full URL out of them.
Slide 17
Slide 17 text
def build_url(base, path, params={})
base << "/" unless path.start_with?('/')
base << path
base << "?" + params.to_param if params.present?
base
end
build_url("http://example.com", "blog")
#=> "http://example.com/blog"
It build simple urls...
Slide 18
Slide 18 text
def build_url(base, path, params={})
base << "/" unless path.start_with?('/')
base << path
base << "?" + params.to_param if params.present?
base
end
build_url("http://example.com", "blog")
#=> "http://example.com/blog"
build_url("http://example.com", "/photos", :sort =>
"size")
#=> "http://example.com/photos?sort=size"
...and ones that are a little more complex.
Slide 19
Slide 19 text
def example_url(path, params={})
build_url("http://example.com", path, params)
end
We then realize we're using the same base URL a lot, so we add another method that has it
hardcoded.
Slide 20
Slide 20 text
def example_url(path, params={})
build_url("http://example.com", path, params)
end
example_url("blog")
#=> "http://example.com/blog"
example_url("/photos", :sort => "size")
#=> "http://example.com/photos?sort=size"
And we get the same URLs as before.
Slide 21
Slide 21 text
ROOT_URL = "http://example.com"
def example_url(path, params={})
build_url(ROOT_URL, path, params)
end
But we don't like the hardcoded URL in the middle of a method, so we pull it out into a
constant (or an env var, or something loaded from a YAML config file).
Slide 22
Slide 22 text
ROOT_URL = "http://example.com"
def example_url(path, params={})
build_url(ROOT_URL, path, params)
end
example_url("blog")
#=> "http://example.com/blog"
The blog URL looks great.
Slide 23
Slide 23 text
ROOT_URL = "http://example.com"
def example_url(path, params={})
build_url(ROOT_URL, path, params)
end
example_url("blog")
#=> "http://example.com/blog"
example_url("/photos", :sort => "size")
#=> "http://example.com/blog/photos?sort=size"
But... the photos URL has "blog" in it. Let's try that again...
DANGER
WILL
ROBINSON
Something has gone very wrong.
Slide 26
Slide 26 text
ROOT_URL = "http://example.com"
def example_url(path, params={})
build_url(ROOT_URL, path, params)
end
def build_url(base, path, params={})
base << "/" unless path.start_with?('/')
base << path
base << "?" + params.to_param if params.present?
base
end
So what's going on?
Slide 27
Slide 27 text
ROOT_URL = "http://example.com"
def example_url(path, params={})
build_url(ROOT_URL, path, params)
end
def build_url(base, path, params={})
base << "/" unless path.start_with?('/')
base << path
base << "?" + params.to_param if params.present?
base
end
We now are only ever making a single instance of the string "http://example.com"
Slide 28
Slide 28 text
ROOT_URL = "http://example.com"
def example_url(path, params={})
build_url(ROOT_URL, path, params)
end
def build_url(base, path, params={})
base << "/" unless path.start_with?('/')
base << path
base << "?" + params.to_param if params.present?
base
end
We then pass this same instance into build_url over and over.
Slide 29
Slide 29 text
ROOT_URL = "http://example.com"
def example_url(path, params={})
build_url(ROOT_URL, path, params)
end
def build_url(base, path, params={})
base << "/" unless path.start_with?('/')
base << path
base << "?" + params.to_param if params.present?
base
end
And in build_url, these shovel operators are mutating this single instance of the string.
Slide 30
Slide 30 text
ROOT_URL = "http://example.com"
def example_url(path, params={})
build_url(ROOT_URL, path, params)
end
def build_url(base, path, params={})
base += "/" unless path.start_with?('/')
base += path
base += "?" + params.to_param if params.present?
base
end
The fix is pretty simple: change the shovels to plus-equals, and instead of mutating `base`,
we'll allocate a new string with the result.
Slide 31
Slide 31 text
ROOT_URL = "http://example.com"
def example_url(path, params={})
build_url(ROOT_URL, path, params)
end
But, the developer who extracted the constant wasn't looking at (and shouldn't have to look
at) `build_url` method to realize that the string would be mutated.
Slide 32
Slide 32 text
ROOT_URL = "http://example.com".freeze
def example_url(path, params={})
build_url(ROOT_URL, path, params)
end
def build_url(base, path, params={})
base += "/" unless path.start_with?('/')
base += path
base += "?" + params.to_param if params.present?
base
end
But, they could have been defensive when we pulled the constant out and frozen us. Doing
this would have caused our broken code to immediately throw an exception pointing out the
mistake.
You probably want to freeze constants like these so you don't unintentionally mutate them.
Slide 33
Slide 33 text
.deep_freeze
with ice_nine
There's one gotcha with freeze: it doesn't freeze the instance variables, collection elements,
etc. of the object you freeze. There's an ice_nine gem that adds a `deep_freeze` method that
will recursively freeze all of these things.
Slide 34
Slide 34 text
Values
Values are objects who's identity is entirely based on the data inside of them, rather than
some external identity. So, ActiveRecord objects are not values, because even if two distinct
AR objects contain the same data, they aren't equal. Their `id`s are what determine their
identity.
Values are also immutable.
You work with values (like numbers and time) every day in Ruby, but you aren't limited to the
value objects that Ruby provides you.
Slide 35
Slide 35 text
Address
210 E 400 S
Salt Lake City, UT 84111
Slide 36
Slide 36 text
Point
(10, 20)
(7, 12, 3)
Slide 37
Slide 37 text
URI
https://braintreepayments.com/docs/ruby
Slide 38
Slide 38 text
github.com/tcrayford/Values
gem install values
There's a great gem for creating value objects called... Values
Slide 39
Slide 39 text
Point = Value.new(:x, :y)
We create a new Point class that has an x and y value. `Value` is a lot like `Struct`, except
that the resulting objects are immutable.
Slide 40
Slide 40 text
Point = Value.new(:x, :y)
origin = Point.new(0, 0)
We make a new point at the origin.
Slide 41
Slide 41 text
Point = Value.new(:x, :y)
origin = Point.new(0, 0)
origin.x #=> 0
origin.y #=> 0
And it's x and y values are both 0.
Slide 42
Slide 42 text
Point = Value.new(:x, :y)
origin = Point.new(0, 0)
origin.x #=> 0
origin.y #=> 0
elsewhere = Point.new(3, 4)
We make another point with some different values.
Slide 43
Slide 43 text
Point = Value.new(:x, :y)
origin = Point.new(0, 0)
origin.x #=> 0
origin.y #=> 0
elsewhere = Point.new(3, 4)
elsewhere.x #=> 3
elsewhere.y #=> 4
It's values pop right out of it.
Slide 44
Slide 44 text
Point = Value.new(:x, :y)
origin = Point.new(0, 0)
origin.x #=> 0
origin.y #=> 0
elsewhere = Point.new(3, 4)
elsewhere.x #=> 3
elsewhere.y #=> 4
elsewhere.x = 10 #=> NoMethodError
But, because these are values, we can't change the data.
Slide 45
Slide 45 text
Point = Value.new(:x, :y)
origin = Point.new(0, 0)
origin.x #=> 0
origin.y #=> 0
elsewhere = Point.new(3, 4)
elsewhere.x #=> 3
elsewhere.y #=> 4
elsewhere.x = 10 #=> NoMethodError
elsewhere == Point.new(3, 4) #=> true
And, the equality is based off of the data inside of it, not any kind of external identity (e.g.
object_id). And, once you determine that two values are identical, you know they'll always be.
Slide 46
Slide 46 text
Point < Value.new(:x, :y)
def to_s
"(#{x}, #{y})"
end
def *(scale)
Point.new(x * scale, y * scale)
end
end
Point.new(1, 2) * 3 #=> "(3, 6)"
Value objects can have "behavior", in the form of convenience methods. These methods can't
modify the internal state of the object though.
Slide 47
Slide 47 text
Deflate bloated
models with
value objects
"Skinny controller, fat model" is something we hear in Rails-land all the time. Having logic in
your models is definitely better than having logic in your controllers, but now there's a
proliferation of apps with "god objects" that have thousands of lines of code and hundreds of
methods. (It's often your `User` class, or some other central model central to your domain).
Value objects are a natural way to pull logic out of your bloated models.
Slide 48
Slide 48 text
class User < ActiveRecord::Base
# t.text :shipping_street
# t.text :shipping_city
# t.text :shipping_state
# t.text :shipping_zip_code
# ...
def calculate_shipping_price
some_calculation
end
end
Let's take a look at how we can decompose one of these bloated models by using value
objects.
We have the canonical bloated model, `User`, and it has attributes for it's shipping address,
and a method to calculate the cost of shipping something to this user.
Slide 49
Slide 49 text
Address = Value.new(
:street,
:city,
:state,
:zip_code
)
We can instead make an Address value to store this data and contain some of this behavior.
Slide 50
Slide 50 text
class User < ActiveRecord::Base
composed_of :shipping_address,
:class_name => "Address",
:mapping => [
["shipping_street", "street"],
["shipping_city", "city"],
["shipping_state", "state"],
["shipping_zip_code", "zip_code"]
]
end
We used ActiveRecord's composed_of helper to map our database fields to the value object's
fields.
Slide 51
Slide 51 text
user.shipping_street = "210 E 400 S"
user.shipping_city = "Salt Lake City"
user.shipping_state = "UT"
user.shipping_zip_code = "84111"
Now, when the fields are assigned to (from a form, or as it comes out of the database)...
Slide 52
Slide 52 text
user.shipping_street = "210 E 400 S"
user.shipping_city = "Salt Lake City"
user.shipping_state = "UT"
user.shipping_zip_code = "84111"
user.shipping_address #=> #
We can ask for the shipping address, and we'll get out an Address.
Slide 53
Slide 53 text
user.shipping_street = "210 E 400 S"
user.shipping_city = "Salt Lake City"
user.shipping_state = "UT"
user.shipping_zip_code = "84111"
user.shipping_address #=> #
user.shipping_address = Address.new(...)
And we can assign a new Address into the field.
Slide 54
Slide 54 text
user.calculate_shipping_price
There's some pain around the original User#calculate_shipping_price method.
Slide 55
Slide 55 text
require 'spec_helper'
describe User do
# hundreds of other tests
describe '#calculate_shipping_price' do
it "calculates the correct price" do
user = FactoryGirl.create(:user,
:shipping_street => "210 E 400 S",
:shipping_city => "Salt Lake City",
:shipping_state => "UT",
:shipping_zip_code => "84111"
)
cost = user.calculate_shipping_price
cost.should == "4.55"
end
end
end
Testing the version of the method that lives on `User` isn't _too_ difficult, but there are a few
unpleasantries.
Slide 56
Slide 56 text
require 'spec_helper'
describe User do
# hundreds of other tests
describe '#calculate_shipping_price' do
it "calculates the correct price" do
user = FactoryGirl.create(:user,
:shipping_street => "210 E 400 S",
:shipping_city => "Salt Lake City",
:shipping_state => "UT",
:shipping_zip_code => "84111"
)
cost = user.calculate_shipping_price
cost.should == "4.55"
end
end
end
We have to include spec_helper, which is going to fire up an entire rails environment and
make our test slow to start.
Slide 57
Slide 57 text
require 'spec_helper'
describe User do
# hundreds of other tests
describe '#calculate_shipping_price' do
it "calculates the correct price" do
user = FactoryGirl.create(:user,
:shipping_street => "210 E 400 S",
:shipping_city => "Salt Lake City",
:shipping_state => "UT",
:shipping_zip_code => "84111"
)
cost = user.calculate_shipping_price
cost.should == "4.55"
end
end
end
We're in a massive file with hundreds of other tests, and we've made it even worse by adding
another.
Slide 58
Slide 58 text
require 'spec_helper'
describe User do
# hundreds of other tests
describe '#calculate_shipping_price' do
it "calculates the correct price" do
user = FactoryGirl.create(:user,
:shipping_street => "210 E 400 S",
:shipping_city => "Salt Lake City",
:shipping_state => "UT",
:shipping_zip_code => "84111"
)
cost = user.calculate_shipping_price
cost.should == "4.55"
end
end
end
We have to use FactoryGirl to build up a model, and we have to talk to the database to save it.
Slide 59
Slide 59 text
user.calculate_shipping_price
vs.
user.address.calculate_shipping_price
But I'm going to propose that this code, while not as succinct or direct, is much nicer in the
long run.
Slide 60
Slide 60 text
describe Address do
describe '#calculate_shipping_price' do
it "calculates the correct price for here" do
address = Address.new(
:street => "489 Elizabeth Street",
:city => "Melbourne",
:state => "VIC",
:postal_code => "3000"
)
cost = address.calculate_price
cost.should == "4.55"
end
end
end
If we have a separate `Shipping` class, then the tests become a lot nicer. There are no
dependencies on external libraries, no special factories, and we end up with both a class and
test quite that are small and isolated.
Slide 61
Slide 61 text
user.calculate_shipping_price
vs.
user.address.calculate_shipping_price
But "nicer tests" isn't a good enough reason for a change like this. Fortunately, it also makes
our application easier to extend.
Slide 62
Slide 62 text
user.calculate_shipping_price
vs.
user.address.calculate_shipping_price
user.addresses[2].calculate_shipping_price
Let's say a user had multiple address, it's obvious how to make the 2nd version work, but I
don't know what I could do to the 1st version that would leave me happy.
Slide 63
Slide 63 text
user.calculate_shipping_price
vs.
user.address.calculate_shipping_price
user.addresses[2].calculate_shipping_price
business.address.calculate_shipping_price
Or, if we want to be able to calculate shipping costs for domain models besides `User`,
again, the 2nd version is incredibly easy to extend, but to make the 1st work, we'd probably
have to extract some sort of `Shippable` module that gets mixed into both `User` and
`Business` and is not at all straightforward to test.
Slide 64
Slide 64 text
user.calculate_shipping_price
vs.
user.address.calculate_shipping_price
user.addresses[2].calculate_shipping_price
business.address.calculate_shipping_price
item.calculate_shipping(user.address)
item.calculate_shipping(user.addresses[2])
item.calculate_shipping(business.address)
And if you decided you needed to have different shipping prices for different items, you could
move the `calculate_shipping_price` to the items, and have the method take an `Address`.
And this change is fairly non-invasive because we're passing around value objects rather then
full blown models.
Slide 65
Slide 65 text
user.shipping_street = user.billing_street
user.shipping_city = user.billing_city
user.shipping_state = user.billing_state
user.shipping_zip_code = user.billing_zip_code
One more example of change that the value-based version is resilient to. Let's think about
how we would implement the "my shipping address is the same as my billing address" check
box.
It's pretty ugly to have to assign each of the address fields individually, and we ever add a
new field to the addresses, it's unlikely that we'd remember to come update this code.
Slide 66
Slide 66 text
user.shipping_address = user.billing_address
If the addresses are composed values, then this can just be a single, intentional line of code.
If we ever add any more fields to address, we don't have to remember to update this
assignment.
Slide 67
Slide 67 text
Event Sourcing
Next up... event sourcing!
Slide 68
Slide 68 text
Capture all changes to
application state as a
sequence of events
Event sourcing is when you capture all changes to an application's state as a sequence of
immutable events.
This is best explained with an example...
Slide 69
Slide 69 text
Opened account $1000
Balance: $1000
Bank accounts are perfect for event sourcing.
You open an account and put $1000 in it.
Slide 70
Slide 70 text
Opened account $1000
Bought conference ticket -$595
Balance: $405
You buy a conference ticket, and your balance goes down.
Slide 71
Slide 71 text
Opened account $1000
Bought conference ticket -$595
Paycheck $4000
Balance: $4405
You get paid; it goes up.
Slide 72
Slide 72 text
Opened account $1000
Bought conference ticket -$595
Paycheck $4000
Bought a book -$15
Balance: $4390
You buy a book...
Slide 73
Slide 73 text
Opened account $1000
Bought conference ticket -$595
Paycheck $4000
Bought a book -$15
Returned the book $15
Balance: $4405
and return it, and your balance go back to what it was before.
Slide 74
Slide 74 text
Events
Debits and credits
(12/6/2012 16:30, "15.00", "Book")
The events in this system are the transactions.
Slide 75
Slide 75 text
Derived state
Balance
And the derived state is the balance.
Slide 76
Slide 76 text
Opened account $1000
Bought conference ticket -$595
Paycheck $4000
Bought a book -$15
Returned the book $15
Balance: $????
We say that the balance is derived, because if I take it away from you, you can recalculate it
from the events (the source of truth).
Slide 77
Slide 77 text
Can reconstruct past
states
What was 110's balance 7 days ago?
We can ask questions about the past. To answer this one, we would just look at all the events
up until 7 days ago, and we'd have our answer.
Slide 78
Slide 78 text
Events can be reverted
Charge was refunded
In an event sourced system, events are reverted (i.e. inserting an opposite event), not deleted
(because they're immutable).
Slide 79
Slide 79 text
Replay
Debug errors & test new code
Events can also be replayed. If there's an error, the banks programmers could grab the event
log and replay it up until the point in time where the error occurred, and they'd have the
system in the exact state it was in in production.
Slide 80
Slide 80 text
git
There's another event sourced system that most of you interact with every day: git!
Slide 81
Slide 81 text
Events
Commits
Commits are the events.
Slide 82
Slide 82 text
Derived state
Working directory
Your working directory is the derived state.
Slide 83
Slide 83 text
Can reconstruct past
states
What did the code look like after
commit a321bd?
You can reconstruct past state and ask questions about the past.
Slide 84
Slide 84 text
Events can be reverted
git revert
git revert, rather than deleting a commit, inserts new commits that do the opposite of the
one you're reverting.
Slide 85
Slide 85 text
Replay
git rebase
And git rebase is a form of replay.
Slide 86
Slide 86 text
Family Tree
At a previous job at a family history startup, we built a family tree feature, and we decided to
event source all of the modifications to the family trees on our site. This turned out to be a
really good decision.
Slide 87
Slide 87 text
Safety net
We wanted to store the family tree in a fancy pants graph database, but we didn't trust it (and
our administration of it) to not lose our data. We stored the event log in Postgres and the
resulting application state in the graph database. If the graph database ever went kaput, we
would still have a canonical version of the data in reliable storage.
Slide 88
Slide 88 text
Audit log
Once or twice, someone vandalized one of the family trees. It was incredibly easy to find all
of the events that the vandal triggered and call the revert! method on them
Slide 89
Slide 89 text
Escape hatch
We eventually decided to move the family tree back into Postgres, and rather than having to
do a complicated ETL to get the data out of the graph DB and into Postgres, we changed our
code to write the computed data into Postgres. We then replayed the entire event log, and our
Postgres DB then held all of our data, in the most recent state.
Slide 90
Slide 90 text
Immutability
lets you break
the rules
There's a bunch of rules in computer science and software engineering that immutability lets
you sidestep.
Slide 91
Slide 91 text
"There are only two hard
problems in Computer
Science:
cache invalidation and
naming things."
There's this famous quote.
Slide 92
Slide 92 text
"There are only two hard
problems in Computer
Science:
cache invalidation and
naming things."
But if your cached data is never going to change, you're never going to have to invalidate it.
Slide 93
Slide 93 text
</
script>
You've probably seen this with the Rails asset pipeline.
Slide 94
Slide 94 text
</
script>
Rails puts a unique version identifier in the url of each version of each asset.
Slide 95
Slide 95 text
</
script>
Cache-Control: public, max-age=31536000
And then tells browsers to cache it for a year (effectively forever in internet time), knowing
that it will never change.
Slide 96
Slide 96 text
Normalization
Why do we normalize our databases?
Slide 97
Slide 97 text
"The objective is to isolate
data so that additions,
deletions, and modifications
of a field can be made in just
one table"
So that we won't have to make updates in more than one place.
Well... if you're not making updates, then you'll never have to do it in more than one place,
and thus normalization isn't necessary.
Slide 98
Slide 98 text
Thread Safety
Thread safety issues are almost entirely caused by shared mutable state. Immutable objects
are automatically thread safe.
Slide 99
Slide 99 text
Downsides
:-(
As with everything, there are tradeoffs when you use immutability.
Slide 100
Slide 100 text
Performance
Due to extra allocations and copying, code that makes use of immutable data will almost
always be slower and use more memory than code that mutates data in-place.
Slide 101
Slide 101 text
Flexibility
You're constraining yourself when you use immutable data. Your domain, your performance
requirements, or libraries you're using might not fit with these constraints.
Slide 102
Slide 102 text
Ruby
Ruby is an incredibly flexible language. Where other languages let you declare variable as
const or final, Ruby will gladly let you reach inside objects and change their instance
variables, reassign constants, and even unfreeze frozen objects.
Slide 103
Slide 103 text
Deletion
Deletion is a form of mutation, and you almost always want user generated data to be
deletable, meaning you can't cache it forever/normalize it, etc. etc.
Slide 104
Slide 104 text
Next steps
http://goo.gl/Esa7r
If you find any of these ideas interesting, I have some pointers to things you can read or
watch or explore to learn more. (This link in the bottom right takes you to a page that has
links to everything I'm about to mention.
Slide 105
Slide 105 text
Clojure
Haskell
Erlang
http://goo.gl/Esa7r
Learn one of these programming languages. Immutability is central to all of them, and they
make you jump through hoops to change state. You might find this impractical for your day
to day programming, but learning at least one of them will help you understand immutability
more deeply and
Slide 106
Slide 106 text
Rich Hickey
The Value of Values
Simple Made Easy
The Database as a Value
Persistent Data Structures and Managed References
http://goo.gl/Esa7r
Rich Hickey is the creator of Clojure, and he has a handful of really good talks centered
around immutability.
Slide 107
Slide 107 text
Value Objects
Domain Driven Design
c2 wiki
http://goo.gl/Esa7r
DDD and the c2 wiki have a lot to say about value objects.
Slide 108
Slide 108 text
Gary Bernhardt
Function Core/Imperative Shell
Boundaries
http://goo.gl/Esa7r
Gary Bernhardt has an interesting idea on how to structure functional/immutable code and
imperative/mutable code in an application together, and he explores this idea in depth in
"Boundaries".
Slide 109
Slide 109 text
Event Sourcing
Martin Fowler
http://goo.gl/Esa7r
Martin Fowler has the canonical text on event sourcing on his website.
Slide 110
Slide 110 text
Thanks!
@michaelfairley
http://goo.gl/Esa7r
Slide 111
Slide 111 text
Bonus round!
Bonus round!
Slide 112
Slide 112 text
Persistent Data
Structures
Persistent Data Structures are immutable data structures. When you "modify" one of them,
you actually get a new copy of the data and the original version remains unchanged.
"Persistent" here shouldn't be confused with the term that means a database writes to disk,
but rather it means that it sticks around.
Slide 113
Slide 113 text
Hamster
github.com/harukizaemon/hamster
Hamster is an awesome implementation of PDTs in Ruby.
Slide 114
Slide 114 text
foo = Hamster.vector(1, 2, 3)
We make a vector (similar to an array) with 1, 2, and 3 in it.
Slide 115
Slide 115 text
foo = Hamster.vector(1, 2, 3)
foo #=> [1, 2, 3]
Slide 116
Slide 116 text
foo = Hamster.vector(1, 2, 3)
foo #=> [1, 2, 3]
bar = foo.add(4)
When we add 4 to it, we assign the result of that into bar.
Slide 117
Slide 117 text
foo = Hamster.vector(1, 2, 3)
foo #=> [1, 2, 3]
bar = foo.add(4)
bar #=> [1, 2, 3, 4]
bar now contains 1, 2, 3, 4
Slide 118
Slide 118 text
foo = Hamster.vector(1, 2, 3)
foo #=> [1, 2, 3]
bar = foo.add(4)
bar #=> [1, 2, 3, 4]
foo #=> [1, 2, 3]
But food still has 1, 2, 3. It hasn't changed.
Slide 119
Slide 119 text
foo = Hamster.vector(1, 2, 3)
foo #=> [1, 2, 3]
bar = foo.add(4)
bar #=> [1, 2, 3, 4]
foo #=> [1, 2, 3]
baz = foo.set(1, 12)
And we can "change" one of the elements.
Slide 120
Slide 120 text
foo = Hamster.vector(1, 2, 3)
foo #=> [1, 2, 3]
bar = foo.add(4)
bar #=> [1, 2, 3, 4]
foo #=> [1, 2, 3]
baz = foo.set(1, 12)
baz #=> [1, 12, 3]
baz has the modification.
To help explain how this is useful, the Three Stooges are going to lend me a hand.
Slide 123
Slide 123 text
m1 = Movie.new(
:name => "Soup to Nuts",
:cast => Hamster.set(:moe, :shemp, :larry)
)
Moe, Shemp, and Larry were in a movie called "Soup to Nuts".
Slide 124
Slide 124 text
m1 = Movie.new(
:name => "Soup to Nuts",
:cast => Hamster.set(:moe, :shemp, :larry)
)
m2 = Movie.new(
:name => "Meet the Baron",
:cast => m1.cast.remove(:shemp).add(:curly)
)
In a later move, "Meet the Baron", Shemp left, and Curly became the 3rd stooge.
Slide 125
Slide 125 text
m1 = Movie.new(
:name => "Soup to Nuts",
:cast => Hamster.set(:moe, :shemp, :larry)
)
m2 = Movie.new(
:name => "Meet the Baron",
:cast => m1.cast.remove(:shemp).add(:curly)
)
m3 = Movie.new(
:name => "Gold Raiders",
:cast => m2.cast.remove(:curly).add(:shemp)
)
And then in "Gold Raiders", Shemp came back, and Curly was out again.
Slide 126
Slide 126 text
m1.cast #=> {:moe, :larry, :shemp}
m2.cast #=> {:moe, :larry, :curly}
m3.cast #=> {:moe, :larry, :shemp}
If we had been using mutable data structures, these cast lists would've clobbered each other
when they were shared between the movies.
Slide 127
Slide 127 text
"So like, Hamster is
just calling .dup a
bunch, right?"
Nope! There's some really cool Computer Science going on here.
Slide 128
Slide 128 text
old = Hamster.vector(1,2,3,4,5,6,7)
1 2 3 4 5 6 7
old
(This is an approximation of what's actually happening)
Here, we have a vector of the numbers 1 through 7. Their actually stored as the leaves of a
tree, and `old` points to the root of this tree.
Slide 129
Slide 129 text
old = Hamster.vector(1,2,3,4,5,6,7)
new = old << 8
1 2 3 4 5 6 7
old
When we append 8 on to this vector.
Slide 130
Slide 130 text
old = Hamster.vector(1,2,3,4,5,6,7)
new = old << 8
1 2 3 4 5 6 7 8
old new
We end up with a new tree. But it's not entirely new.
Slide 131
Slide 131 text
old = Hamster.vector(1,2,3,4,5,6,7)
new = old << 8
1 2 3 4 5 6 7 8
old new
All of the nodes in red are shared between both the old and the new version of the vector.
This minimizes both the CPU and memory requirements for these data structures (as
opposed to .duping them).