Skip to content

tonsser/takes_macro

Repository files navigation

TakesMacro

ci badge

attr_extras is a great gem that lets you remove most of the boilerplate needed for creating different types of initializers in Ruby.

This gem contains a reimplementation of pattr_initialize from attr_extras that is much faster. If you're using attr_extras, but the only feature of it you're using is pattr_initialize then this gem is for you.

This gem calls the method takes to avoid confusing, but the API is exactly the same.

Benchmark

Run bundle exec ruby benchmarks/takes_macro_vs_attr_extras.rb to see how much faster this gem is. The output from doing that on my machine is:

$ bundle exec ruby benchmarks/takes_macro_vs_attr_extras.rb
Warming up --------------------------------------
         attr_extras    15.793k i/100ms
             takes_macro    91.596k i/100ms
hand written initializer
                        71.351k i/100ms
Calculating -------------------------------------
         attr_extras    169.353k (± 3.9%) i/s -    852.822k in   5.043680s
             takes_macro      1.209M (± 4.5%) i/s -      6.045M in   5.011814s
hand written initializer
                        875.721k (± 5.8%) i/s -      4.424M in   5.071249s

Comparison:
                   takes_macro: 1208748.5 i/s
  hand written initializer:     875721.1 i/s - 1.38x  slower
               attr_extras:     169352.8 i/s - 7.14x  slower

The initializer generated by takes is faster than the hand written version because the takes version uses an options hash, and the hand written version uses keyword arguments, which are a bit slower. You can see the exact code for the benchmark here.

How it works

This gem expands this

class A
  takes [:foo!]
end

Into this

class A
  def initialize(options)
    @foo = options.fetch(:foo)
  end

  attr_reader :foo
  private :foo
end

It does that by looking at the arguments to takes and from that building a String of Ruby code that it will then class_eval. That means calling takes is literally as fast as writing the initializer by hand. Nothing fancy happens when you call A.new(...).

Possible arguments to takes

You can call takes in many different ways depending on the style of initializer you want. Here are the different styles and what they expand into:

Positional args

takes :foo, :bar

def initialize(foo, bar)
  @foo = foo
  @bar = bar
end

Required keyword args

takes [:foo!, :bar!]

def initialize(foo:, bar:)
  @foo = foo
  @bar = bar
end

Optional keyword args

takes [:foo, :bar]

def initialize(foo: nil, bar: nil)
  @foo = foo
  @bar = bar
end

Mixed positional, required and optional keyword args

takes :foo, [:bar!, :baz]

def initialize(foo, bar:, baz: nil)
  @foo = foo
  @bar = bar
  @baz = baz
end

Note: Each instance variable set in the initializer also gets a private attr_reader, but that was left out of the examples for clarity.

Installation

Add this line to your application's Gemfile:

gem "takes_macro"

And then execute:

$ bundle

Or install it yourself as:

$ gem install takes_macro

Usage

If you want to use takes in all your classes add this to your app:

require "takes_macro"

TakesMacro.monkey_patch_object

If you're in a Rails app I recommend adding this to config/application.rb so you're sure to have it in all your classes.

That will include the TakesMacro module, which defines the takes method, on Object.

If you don't like monkey patching Object you can still do this:

require "takes_macro"

class A
  include TakesMacro

  takes [:foo!, :bar!, :baz!]
end

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.