Metaprogramming and Folly
-
Upload
haseeb-qureshi -
Category
Software
-
view
301 -
download
0
Embed Size (px)
Transcript of Metaprogramming and Folly

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