$30 off During Our Annual Pro Sale. View Details »

The Value of Being Lazy

Erik Berlin
November 24, 2015

The Value of Being Lazy

…or How I Made OpenStruct 10X Faster

Presented at Rails Israel 2015.

Erik Berlin

November 24, 2015
Tweet

More Decks by Erik Berlin

Other Decks in Programming

Transcript

  1. THE VALUE OF BEING LAZY

    or How I Made OpenStruct 10X Faster
    Erik Michaels-Ober
    @sferik

    View Slide

  2. In Ruby, everything is an object.
    ∀ thing

    thing.is_a?(Object) #=> true

    View Slide

  3. In Ruby, every object has a class.
    ∀ object

    object.respond_to?(:class) #=> true

    View Slide

  4. In Ruby, every class has a class.
    ∴

    Object.respond_to?(:class) #=> true
    Object.class #=> Class

    View Slide

  5. You can use classes to create new objects:
    object = Object.new

    object.class #=> Object

    View Slide

  6. You can use classes to create new classes:
    klass = Class.new

    klass.class #=> Class

    View Slide

  7. Usually, we create classes like this:
    class Point
    attr_accessor :x, :y
    def initialize(x, y)
    @x, @y = x, y
    end
    end

    View Slide

  8. You can replace such simple classes with structs:
    Point = Struct.new(:x, :y)

    View Slide

  9. OpenStruct requires even less definition:
    point = OpenStruct.new
    point.x = 1

    point.y = 2

    View Slide

  10. In this way, OpenStruct is similar to Hash:
    point = Hash.new
    point[:x] = 1

    point[:y] = 2

    View Slide

  11. You can even initialize OpenStruct with a Hash:
    point = OpenStruct.new(x: 1, y: 2)
    point.x #=> 1

    point.y #=> 2

    View Slide

  12. So why use OpenStruct instead of Hash?

    View Slide

  13. Test double
    validator = OpenStruct.new
    expect(validator).to receive(:validate)
    code = PostalCode.new("94102", validator)
    code.valid?

    View Slide

  14. API response
    user = OpenStruct.new(JSON.parse(response))
    user.name #=> Erik

    View Slide

  15. Configuration object
    def options
    opts = OpenStruct.new
    yield opts
    opts
    end

    View Slide

  16. So OpenStruct is useful…but slow.

    View Slide

  17. View Slide

  18. Steps to optimize code
    1. Complain that code is slow on Twitter
    2. ???
    3. Profit

    View Slide

  19. Actual steps to optimize code
    1. Benchmark
    2. Read code
    3. Profit

    View Slide

  20. Actual steps to optimize code
    1. Benchmark
    2. Read code
    3. Profit

    View Slide

  21. require "benchmark/ips"

    Point = Struct.new(:x, :y)
    def struct
    Point.new(0, 1)
    end

    def ostruct
    OpenStruct.new(x: 0, y: 1)
    end

    Benchmark.ips do |x|
    x.report("ostruct") { ostruct }
    x.report("struct") { struct }
    end

    View Slide

  22. Comparison:
    struct: 2927800.2 i/s
    ostruct: 84741.1 i/s - 34.55x slower

    View Slide

  23. Actual steps to optimize code
    1. Benchmark
    2. Read code
    3. Profit

    View Slide

  24. def initialize(hash = nil)
    @table = {}
    if hash
    hash.each_pair do |k, v|
    k = k.to_sym
    @table[k] = v
    new_ostruct_member(k)
    end
    end
    end

    View Slide

  25. def new_ostruct_member(name)
    name = name.to_sym
    unless respond_to?(name)
    define_singleton_method(name) { @table[name] }
    define_singleton_method("#{name}=") { |x| @table[name] = x }
    end
    name
    end

    View Slide

  26. def method_missing(mid, *args)
    len = args.length
    if mname = mid[/.*(?==\z)/m]
    @table[new_ostruct_member(mname)] = args[0]
    elsif len == 0
    if @table.key?(mid)
    new_ostruct_member(mid)
    @table[mid]
    end
    end
    end

    View Slide

  27. def initialize(hash = nil)
    @table = {}
    if hash
    hash.each_pair do |k, v|
    k = k.to_sym
    @table[k] = v
    new_ostruct_member(k)
    end
    end
    end

    View Slide

  28. Before:
    struct: 2927800.2 i/s
    ostruct: 84741.1 i/s - 34.55x slower

    View Slide

  29. After:
    struct: 2927800.2 i/s
    ostruct: 940170.4 i/s - 3.11x slower

    View Slide

  30. View Slide

  31. View Slide

  32. git log --reverse lib/ostruct.rb

    View Slide

  33. View Slide

  34. Lazy evaluation

    View Slide

  35. Enumerator::Lazy

    View Slide

  36. lazy_integers = (1..Float::INFINITY).lazy
    lazy_integers.collect { |x| x ** 2 }.
    select { |x| x.even? }.
    reject { |x| x < 1000 }.
    first(5)
    #=> [1024, 1156, 1296, 1444, 1600]

    View Slide

  37. require "prime"
    lazy_primes = Prime.lazy
    lazy_primes.select { |x| (x - 2).prime? }.
    collect { |x| [x - 2, x] }.
    first(5)
    #=> [[3, 5], [5, 7], [11, 13], [17, 19], [29, 31]]

    View Slide

  38. module Enumerable
    def repeat_after_first
    unless block_given?
    return to_enum(__method__) { size * 2 - 1 if size }
    end
    each.with_index do |*val, index|
    index == 0 ? yield *val : 2.times { yield *val }
    end
    end
    end

    View Slide

  39. require "prime"
    lazy_primes = Prime.lazy
    lazy_primes.repeat_after_first.
    each_slice(2).
    select { |x, y| x + 2 == y }.
    first(5)
    #=> [[3, 5], [5, 7], [11, 13], [17, 19], [29, 31]]

    View Slide

  40. require "date"
    lazy_dates = (Date.today..Date.new(9999)).lazy
    lazy_dates.select { |d| d.day == 13 }.
    select { |d| d.friday? }.
    first(10)

    View Slide

  41. lazy_file = File.readlines("/path/to/file").lazy
    lazy_file.detect { |x| x =~ /regexp/ }

    View Slide

  42. Being lazy is efficient.

    View Slide

  43. Being lazy is elegant.

    View Slide

  44. Thanks to:

    Zachary Scott
    ROSS Conf
    Rails Israel

    View Slide

  45. Thank you

    View Slide