Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Immutable Ruby

Immutable Ruby

Michael Fairley

April 04, 2013
Tweet

More Decks by Michael Fairley

Other Decks in Programming

Transcript

  1. Immutable Ruby
    Michael Fairley
    @michaelfairley

    View Slide

  2. I work at Braintree Payments. We make it super easy for businesses of any size to take credit
    card payments online.

    View Slide

  3. We handle payments for thousand of really awesome companies (like these).

    View Slide

  4. 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.

    View Slide

  5. 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.

    View Slide

  6. 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.

    View Slide

  7. class Purchase < ActiveRecord::Base
    include Immutable
    end
    It would be nice if we could mix in a module that makes this model immutable.

    View Slide

  8. 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.

    View Slide

  9. 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.

    View Slide

  10. 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.

    View Slide

  11. 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.

    View Slide

  12. 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.

    View Slide

  13. 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...

    View Slide

  14. 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.

    View Slide

  15. In your
    application too!
    You application code also has a bunch of data that you don't really want to change.

    View Slide

  16. 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.

    View Slide

  17. 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...

    View Slide

  18. 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.

    View Slide

  19. 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.

    View Slide

  20. 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.

    View Slide

  21. 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).

    View Slide

  22. 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.

    View Slide

  23. 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...

    View Slide

  24. 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"
    example_url("/photos", :sort => "size")
    #=> "http://example.com/blog/photos?sort=size/
    photos?sort=size"
    And things don't look great. :-/

    View Slide

  25. DANGER
    WILL
    ROBINSON
    Something has gone very wrong.

    View Slide

  26. 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?

    View Slide

  27. 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"

    View Slide

  28. 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.

    View Slide

  29. 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.

    View Slide

  30. 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.

    View Slide

  31. 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.

    View Slide

  32. 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.

    View Slide

  33. .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.

    View Slide

  34. 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.

    View Slide

  35. Address
    210 E 400 S
    Salt Lake City, UT 84111

    View Slide

  36. Point
    (10, 20)
    (7, 12, 3)

    View Slide

  37. URI
    https://braintreepayments.com/docs/ruby

    View Slide

  38. github.com/tcrayford/Values
    gem install values
    There's a great gem for creating value objects called... Values

    View Slide

  39. 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.

    View Slide

  40. Point = Value.new(:x, :y)
    origin = Point.new(0, 0)
    We make a new point at the origin.

    View Slide

  41. 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.

    View Slide

  42. 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.

    View Slide

  43. 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.

    View Slide

  44. 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.

    View Slide

  45. 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.

    View Slide

  46. 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.

    View Slide

  47. 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.

    View Slide

  48. 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.

    View Slide

  49. 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.

    View Slide

  50. 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.

    View Slide

  51. 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)...

    View Slide

  52. 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 #=> #0x007f7fd4a3dee0 @street="210 E 400 S",
    @city="Salt Lake City", @state="UT",
    @zip_code="84111">
    We can ask for the shipping address, and we'll get out an Address.

    View Slide

  53. 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 #=> #0x007f7fd4a3dee0 @street="210 E 400 S",
    @city="Salt Lake City", @state="UT",
    @zip_code="84111">
    user.shipping_address = Address.new(...)
    And we can assign a new Address into the field.

    View Slide

  54. user.calculate_shipping_price
    There's some pain around the original User#calculate_shipping_price method.

    View Slide

  55. 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.

    View Slide

  56. 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.

    View Slide

  57. 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.

    View Slide

  58. 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.

    View Slide

  59. 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.

    View Slide

  60. 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.

    View Slide

  61. 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.

    View Slide

  62. 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.

    View Slide

  63. 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.

    View Slide

  64. 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.

    View Slide

  65. 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.

    View Slide

  66. 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.

    View Slide

  67. Event Sourcing
    Next up... event sourcing!

    View Slide

  68. 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...

    View Slide

  69. Opened account $1000
    Balance: $1000
    Bank accounts are perfect for event sourcing.
    You open an account and put $1000 in it.

    View Slide

  70. Opened account $1000
    Bought conference ticket -$595
    Balance: $405
    You buy a conference ticket, and your balance goes down.

    View Slide

  71. Opened account $1000
    Bought conference ticket -$595
    Paycheck $4000
    Balance: $4405
    You get paid; it goes up.

    View Slide

  72. Opened account $1000
    Bought conference ticket -$595
    Paycheck $4000
    Bought a book -$15
    Balance: $4390
    You buy a book...

    View Slide

  73. 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.

    View Slide

  74. Events
    Debits and credits
    (12/6/2012 16:30, "15.00", "Book")
    The events in this system are the transactions.

    View Slide

  75. Derived state
    Balance
    And the derived state is the balance.

    View Slide

  76. 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).

    View Slide

  77. 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.

    View Slide

  78. 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).

    View Slide

  79. 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.

    View Slide

  80. git
    There's another event sourced system that most of you interact with every day: git!

    View Slide

  81. Events
    Commits
    Commits are the events.

    View Slide

  82. Derived state
    Working directory
    Your working directory is the derived state.

    View Slide

  83. Can reconstruct past
    states
    What did the code look like after
    commit a321bd?
    You can reconstruct past state and ask questions about the past.

    View Slide

  84. 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.

    View Slide

  85. Replay
    git rebase
    And git rebase is a form of replay.

    View Slide

  86. 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.

    View Slide

  87. 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.

    View Slide

  88. 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

    View Slide

  89. 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.

    View Slide

  90. Immutability
    lets you break
    the rules
    There's a bunch of rules in computer science and software engineering that immutability lets
    you sidestep.

    View Slide

  91. "There are only two hard
    problems in Computer
    Science:
    cache invalidation and
    naming things."
    There's this famous quote.

    View Slide

  92. "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.

    View Slide

  93. <br/>script><br/>You've probably seen this with the Rails asset pipeline.<br/>

    View Slide

  94. <br/>script><br/>Rails puts a unique version identifier in the url of each version of each asset.<br/>

    View Slide

  95. <br/>script><br/>Cache-Control: public, max-age=31536000<br/>And then tells browsers to cache it for a year (effectively forever in internet time), knowing<br/>that it will never change.<br/>

    View Slide

  96. Normalization
    Why do we normalize our databases?

    View Slide

  97. "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.

    View Slide

  98. Thread Safety
    Thread safety issues are almost entirely caused by shared mutable state. Immutable objects
    are automatically thread safe.

    View Slide

  99. Downsides
    :-(
    As with everything, there are tradeoffs when you use immutability.

    View Slide

  100. 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.

    View Slide

  101. 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.

    View Slide

  102. 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.

    View Slide

  103. 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.

    View Slide

  104. 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.

    View Slide

  105. 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

    View Slide

  106. 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.

    View Slide

  107. Value Objects
    Domain Driven Design
    c2 wiki
    http://goo.gl/Esa7r
    DDD and the c2 wiki have a lot to say about value objects.

    View Slide

  108. 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".

    View Slide

  109. Event Sourcing
    Martin Fowler
    http://goo.gl/Esa7r
    Martin Fowler has the canonical text on event sourcing on his website.

    View Slide

  110. Thanks!
    @michaelfairley
    http://goo.gl/Esa7r

    View Slide

  111. Bonus round!
    Bonus round!

    View Slide

  112. 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.

    View Slide

  113. Hamster
    github.com/harukizaemon/hamster
    Hamster is an awesome implementation of PDTs in Ruby.

    View Slide

  114. foo = Hamster.vector(1, 2, 3)
    We make a vector (similar to an array) with 1, 2, and 3 in it.

    View Slide

  115. foo = Hamster.vector(1, 2, 3)
    foo #=> [1, 2, 3]

    View Slide

  116. 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.

    View Slide

  117. 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

    View Slide

  118. 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.

    View Slide

  119. 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.

    View Slide

  120. 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.

    View Slide

  121. 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]
    foo #=> [1, 2, 3]
    But foo remains the same.

    View Slide

  122. To help explain how this is useful, the Three Stooges are going to lend me a hand.

    View Slide

  123. 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".

    View Slide

  124. 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.

    View Slide

  125. 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.

    View Slide

  126. 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.

    View Slide

  127. "So like, Hamster is
    just calling .dup a
    bunch, right?"
    Nope! There's some really cool Computer Science going on here.

    View Slide

  128. 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.

    View Slide

  129. 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.

    View Slide

  130. 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.

    View Slide

  131. 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).

    View Slide