the black magic of ruby metaprogramming

Post on 08-May-2015

2.189 Views

Category:

Technology

0 Downloads

Preview:

Click to see full reader

DESCRIPTION

What is metaprogramming is and how can we use it in our everyday code - by Cristian Planas, CTO at Playfulbet

TRANSCRIPT

The black magic ofRuby

metaprogramming

I am Cristian…

Gawyn

@cristianplanas

… and this is the story of how I fell in

love with Ruby

It was 2011, when I read this awesome book

(the smart part of this talk is based on it)

I was so excited that I created my first gem

just to play with metaprogramming.

Easyregexp

http://github.com/Gawyn/easyregexp

It was a regular expressions generator.

It relied heavily in metaprogramming.

It was never much useful.

Easyregexp

Anyway, I felt like this

I mean, people who use metaprogramming do

have superpowers.

They even know mysterious,

incomprehensible spells!

The superclass of the eigenclass of an object is the object’s class.

The superclass of the eigenclass of a class is the eigenclass of the

class’s superclass.

Repeat with me

Sorry, today we won’t talk about

eigenclasses.

We will focus on the most down to earth

part of metaprogramming.

When I started using metaprogramming in my production code, I remembered an old

movie.

In it, Mickey Mouse is the hard-working apprentice of a powerful sorcerer.

One day, the sorcerer leaves, and Mickey can

play with magic…

So let’s learn some tricks!

Monkey patching

Adding code to an already defined class.

Monkey patching

class String

def say_hello

p “hello”

end

end

“an string”. say_hello

=> “hello”

An example

This means that we can extend any class with

our own methods.

You can also redefine an already existing

method!

Dynamic methods

Define methods with an algorythm in runtime.

Dynamic methods

Imagine a class full of boring, repeating methods.

A typical example

class User

ROLES = [“user”, “admin”]

# scopes for each role

scope :user, where(role: “user”)

scope :admin, where(role: “admin”)

# Identifying methods for each role

def user?

role == “user”

end

def admin?

role == “admin”

end

end

A typical example

Now let’s add some more roles:

ROLES = [“guest”, “user”, “confirmed_user”,

“moderator”, “manager”, “admin”, “superadmin”]

A typical example

Damn!

Metaprogramming to the rescue!

A typical example

class User

ROLES = [“guest”, “user”, “confirmed_user”, “moderator”, “manager”, “admin”, “superadmin”]

ROLES.each do |user_role|

scope user_role.to_sym, where(role: user_role)

define_method “#{user_role}?” do

role == user_role

end

end

end

A typical example

Great!

Evaluating strings as code

It does just that: run a string as if it was code.

class_eval and instance_eval do the same, just changing the

context.

We can also use it on an object using send.

class Movie

attr_reader :title_en, :title_es, :title_it, :title_pt

def title

send(“title_#{I18n.locale}”)

end

end

An example

class Movie

translate :title, :overview

def translate(*attributes)

attributes.each do |attr|

define_method attr do

send(“#{attr}_#{I18n.locale}”)

end

end

end

end

An example

You can even define new methods like this!

String.class_eval(“def say_hello; puts ‘hello’; end”)

“a string”.say_hello

#=> “hello”

method_missing

It’s the method that gets executed when the called

method it’s not found.

method_missing

We can monkey patch it!

class MetalDetector

def method_missing(method, *args, &block)

if method =~ /metal/

puts “Metal detected!”

else

super

end

end

end

metal_detector = MetalDetector.new

metal_detector.bringing_some_metal_with_me

# => “Metal detected!”

An example

A pretty exemplary use of method_missing use

are Rails’ dynamic finders

(deprecated in Rails 4)

Calling finding methods with any combination of attributes will work.

User.find_by_name_and_surname(“Mickey”, “Mouse”)

User.find_by_surname_and_movie(“Mouse”, “Fantasia”)

User.find_by_movie_and_job_and_name(“Fantasia”, “sorcerer”, “Mickey”)

So metaprogramming is pretty cool, isn’t it?

It can get cooler

Let’s check how ActiveRecord defines

its setter methods

(in an abbreviated version)

def method_missing(method, *args, &block)

unless self.class.attribute_methods_generated?

self.class.define_attribute_methods

if respond_to_without_attributes?(method)

send(method, *args, &block)

else

super

end

else

super

end

end

Defining setters

def define_write_method(attr_name)

method_definition = “def #{attr_name}=(new_value); write_attribute(‘#{attr_name}’, new_value); end”

class_eval(method_definition, __FILE__, __LINE)

end

Finally it gets to something like this:

(as told, it’s a pretty abbreviated version)

To know more

Episode 8 of Metaprogramming Ruby: Inside ActiveRecord

https://github.com/rails/rails/blob/master/activerecord/lib/active_record/attribute_methods.rb

https://github.com/rails/rails/blob/master/activemodel/lib/active_model/attribute_methods.rb

https://github.com/rails/rails/blob/master/activerecord/lib/active_record/attribute_methods/write.rb

But in the end of the movie, all the magic backslashes…

Metaprogramming has its dangers

Unexpected method override

It happens when you rewrite an existing method changing its

behavior without checking the consequences.

That means all the code that rely in the old method behavior

will fail.

class Fixnum

alias :old_minus :-

alias :- :+

alias :+ :old_minus

end

4 + 3

#=> 1

5 – 2

#=> 7

Dynamic methods like the ones of the example can also

accidentally monkey patch critical methods!

Code injection through

evaluation

If you use any kind of eval, remember to think in all

possible cases, specially if users are involved.

You don’t want to evaluate “User.destroy_all” on your own

application!

Even if users should be able to evaluate code in your server,

there are ways to protect you:

Clean Rooms

Ghost methods

Methods working from inside method_missing don’t “really”

exist for Ruby.

class MetalDetector

def method_missing(method, *args)

if method =~ /metal/

puts “Metal detected!”

else

super

end

end

end

metal_detector = MetalDetector.new

metal_detector.metal

# => “Metal detected!”

metal_detector.respond_to?(:metal)

#=> false

There is a work-around: to monkey patch the respond_to?

method.

Feels kinda hacky.

And still, it can be hard to maintain.

If you want to know more about the

dangers of method_missing, Paolo

Perrotta (the author of

Metaprogramming Ruby) has a full

presentation about it: The revenge of

method_missing()

Some final thoughts

1. I look pretty cool with Superman trunks.

2. Metaprogramming is a name for different

techniques: you can use some and avoid others.

Personally, I use plenty of dynamic methods and avoid method_missing.

3. Just be sure of what you do when you use it.

The sorcerer won’t come to save you!

Thanks!

top related