metaprogramming and folly

47
Metaprogramming and Folly HASEEB QURESHI SOFTWARE ENGINEER @

Upload: haseeb-qureshi

Post on 12-Apr-2017

307 views

Category:

Software


0 download

TRANSCRIPT

Metaprogramming and Folly

HASEEB QURESHI

SOF TWARE ENGINEER @

What is metaprogramming?

Metaprogramming is code that writes code.

You use it all the time.

class Player attr_accessor :healthend

You use it all the time.

class Player def health @health end

def health=(new_health) @health = new_health endend

Macros are the simplest form of metaprogramming.

attr_reader

alias_method

def_delegators

And if we include Rails…

belongs_to

has_many_through

scope

before_filter

When they’re a predictable part of our common language,

they don’t really seem like metaprogramming.

*********** ********** ** * ** ***** * ** * * * ** * * * * ** * *** * ** * * ** ******* ** ************

But when we talk about metaprogramming, we’re

usually referring to “magic.”

Let’s talk about magic.

Object#send

:send is the hook into Ruby’s method dispatch.

[1, 2, 3].sort_by { |x| x.hash.hash } [1, 2, 3].send("sort_by") { |x| x.hash.hash }

"hello".reverse

"hello".send(:reverse)

It also allows you to access private methods.

class Player # ... private

def top_secret_password "hunter12" endend

Player.new.send(:top_secret_password) #=> "hunter12"

So that’s *mostly* useless.

But the real power of :send is dynamic dispatch.

Let’s look at this player class.class Player UI_ACTIONS = [:attack, :defend, :retreat]

def attack end

def defend end

def retreat end

def other_method endend

Say our player gets a form:

<select> <% Player::UI_ACTIONS.each do |action| %> <option value="<%= action %>"> <%= action.capitalize %> </option> <% end %></select>

Instead of doing this…

case params[:action]when 'attack' current_player.attackwhen 'defend' current_player.defendwhen 'retreat' current_player.retreatend

if Player::UI_ACTIONS.include?(params[:action])

endcurrent_player.send(params[:action])

We do this.

String#constantize

(ActiveSupport only.)

:constantize allows you to turn strings into constants

(including classes).

(It’s essentially a special case of :eval.)

Imagine you have some POROs in your app.

class Sword < Weapon def self.damage 10 endend

class Spear < Weapon def self.damage 6 endend

class Dagger < Weapon def self.damage 4 endend

class StronglyWordedEmail < Weapon def self.damage 1 endend

This is a common pattern:

Player.first.weapon_type.constantize.damage # => 4

Player: { id: 1, hp: 50, weapon_type: 'Dagger',}

Class#define_method

:define_method allows you to create new instance

methods at runtime.

class Player CHAINABLE_MOVES = [:slash, :swipe, :poke]

def slash 5 end

def swipe 3 end

def poke 1 endend

class Player CHAINABLE_MOVES.permutation(2).each do |m1, m2| define_method("#{m1}_and_#{m2}") do send(m1) + send(m2) end endend

p Player.new.methods - [].methods # => [:slash, :swipe, :poke, :slash_and_swipe, :slash_and_poke, :swipe_and_slash, :swipe_and_poke, :poke_and_slash, :poke_and_swipe]

p Player.new.poke_and_slash # => 6

Now this is definitely magical.

But we can go deeper.

Object#method_missing

:method_missing is like a before_filter to NoMethodErrors.

If you call a method that doesn’t exist, :method_missing is first

invoked.

class Player def attack puts "Hiya!" end

def defend puts "Ouch." end

def retreat puts "AHHHHHHHHH" endend

Player.new.triple_attack => undefined method `triple_attack' for #<Player:0x007f971b8291a0> (NoMethodError)

class Player def method_missing(m, *args) if m =~ /^triple_(\w+)/ && respond_to?($1) 3.times { send($1) } else puts "I can't do that..." end endend

Player.new.triple_attack => Hiya!=> Hiya!=> Hiya!

Let’s just add this.

Pretty cool, right?

Actually, this is terrible.

Never do this.

Most metaprogramming has no place in a production codebase:

method_missingeval

instance_evalclass_eval

instance_variable_setconst_set

Let’s talk why.

Pros:

• Powerful DSLs

- You think you want this, but you probably don’t.

• Keeps things DRY!

- Be careful. You can go overboard.

Cons:• Greppability?

• Greppability.

• Greppability…

• Greppability!!

class Player def method_missing(m, *args) if m =~ /^triple_(\w+)/ && respond_to?($1) 3.times { send($1) } else puts "I can't do that..." end endend

> grep triple_attack *Player.rb:21:Player.new.triple_attack> ...wtf

Cons:

• It creates a high cognitive load on anyone entering

your codebase.

• Makes debugging harder, stack traces more

mysterious, static analysis harder.

• It can easily commit you to the wrong abstractions.

Metaprogramming is magical.

But true magic demands that we all believe in it.

Thanks for listening.

You can follow me at @hosseeb