Skip to content

Latest commit

 

History

History
611 lines (456 loc) · 13.1 KB

slides.md

File metadata and controls

611 lines (456 loc) · 13.1 KB

layout: true

RubyConf

2016-11-10

@CraigBuchek


class: title, middle, center

A Look at Hooks

Craig Buchek

???

  • Feel free to ask questions during!

class: affiliations

About Me

???

  • I have a company called BoochTek
    • We do:
      • Web Development
      • Rails rescue projects
      • Agile player/coaching
      • DevOps
  • I do a lot of work through Binary Noggin
    • We have developers available starting in January
  • I participate in a podcast called This Agile Life
    • Please subscribe
  • I mentor teaching underprivileged kids
  • I was told there must be at least 1 cat picture
    • So there's a picture of my cat

class: what_is_a_hook

What Is a Hook?

  • A method:
    • Called implicitly by Ruby
    • We never call ourselves

???

  • Definition I'm using in this talk:
    • A method that's called implicitly by Ruby
    • And that we never call ourselves
  • Because it's implicit:
    • Can be surprising
    • Can be difficult to troubleshoot
  • "Spooky action at a distance"
    • A term from physics
  • In Ruby docs, you'll see them referenced as:
    • "Hook"
    • "Callback"
      • But that has more meanings
    • "Ruby calls"
  • I'm covering only hooks in Ruby itself

BasicObject#initialize

class X
  def initialize
    puts "Initializing an instance of X"
  end
end

x = X.new
Initializing an instance of X

???

  • Who here has used initialize?
    • Pretty much everyone, probably
  • Notice that we never explicitly called initialize
    • But it got called anyway
  • This is by far the most commonly used hook in Ruby

Class#new: Behind the Scenes

x = X.new
class Class
  def new(*args, &block)
    object = allocate # Allocate space in memory to hold the object
    object.initialize(*args, &block) if object.respond_to?(:initialize)
    return object
  end
end
  • X is an object of class Class
    • So calling X.new calls Class#new

???

  • So what's really happening when we call X.new?
  • X is an object of class Class
    • So calling X.new calls Class#new
    • Seems weird, but how every method call works
      • Find the class of what's before the dot
      • Call the instance method from that class
  • Because the call to initialize is hidden, it's a hook
  • NOTE: This code is actually implemented in C
    • It also calls initialize via __send__
      • So it will work if initialize is private
  • NOTE: Understanding that classes are instances of the Class class is tricky
    • If you need to, take some time to work through thinking about it

Meta-Programming Basics

  • method_missing
  • respond_to_missing?

???

  • Meta-programming is where you're most likely to use hooks

BasicObject#method_missing

class X
  def method_missing(method_name, *args, &block)
    puts "Method called on an X: #{method_name}, args: #{args}"
  end
end

X.new.some_method(1, "a", :b)
X.new.another_method
Method called on an X: some_method, args: [1, "a", :b]
Method called on an X: another_method, args: []

???

  • How many of you have used method_missing?
    • Probably a large percentage
  • We're allowing the calling of a method that we never defined
  • Pretty commonly used for meta-programming
  • ActiveRecord uses method_missing in several places
  • Biggest downside --- you can't search your code for the method
  • The method name is passed as a Symbol
  • We use *args to get all the arguments as an Array
    • All arguments except a block (if one is passed in)
    • The * is called "splat"
  • NOTE: The block isn't being used in this example
    • I just wanted to show the full method signature

Object#respond_to_missing?

class X
  def method_missing(method_name, *args, &block)
    puts "Method called on an X: #{method_name}, args: #{args}" \
          if respond_to_missing?(method)
  end

  def respond_to_missing?(method_name, include_private=false)
    method_name =~ /^good_/
  end
end
X.new.respond_to?(:good_method)
X.new.respond_to?(:bad_method)
true
false

???

  • Called when respond_to? is called, and the method is not defined
    • Before Ruby 1.9.2, we used to just override respond_to?
  • Called when calling method
  • Note that it takes a 2nd argument
    • Whether to include private methods
  • Always define respond_to_missing? when overriding method_missing
  • See a nice explanation at http://blog.marc-andre.ca/2010/11/15/methodmissing-politely/
  • Yes, this is on Object, while method_missing is on BasicObject
    • Because BasicObject doesn't have respond_to?

Module#const_missing

  • Avoid this one
  • Seems enticing, but leads to long-term problems
  • Use require instead
    • Or maybe autoload

???

  • Rails uses this to auto-load classes
    • Remember, class names are constants
  • In general, it's not worth it
    • Just manually require the file containing the class/constant you need
  • If you really want something like this, try registering with autoload instead
    • autoload(:MyModule, "/usr/local/lib/modules/my_module.rb")

Inheritance Life-Cycle

  • included
  • extended
  • prepended
  • inherited

Module#included

module A
  def self.included(other_module)
    puts "#{self} included in #{other_module}"
  end
end

class B
  include A
end
A included in B

???

  • Note that it's def self.included
    • Because it's a method on the module itself
  • Note that we don't need to new up a B
    • The include (and the included hook) runs as soon as we call it
    • TODO: Maybe in a next slide show how that works with normal code
  • Called when this module is included in another module or class
  • Called with the class or module that is including us
  • NOTE: There's a similar hook called append_features
    • It adds constants, methods, and module variables of this module to module passed in
    • I know of no reason to use append_features over included
      • Unless you want to do something nefarious and NOT add constants/methods/variables

Module#extended

module A
  def self.extended(other_module)
    puts "#{self} extended by #{other_module}"
  end
end

module B
  extend A
end
A extended by B

???

  • Just like included
  • Note that it's def self.extended

Module#prepended

module A
  def self.prepended(other_module)
    puts "#{self} prepended in #{other_module}"
  end
end

class B
  prepend A
end
A prepended in B

???

  • Ruby added prepend in 2.0
  • Just like included
  • Note that it's def self.prepended
    • Because it's a method on the module itself

Class#inherited

class A
  def self.inherited(other_class)
    puts "#{self} subclassed by #{other_class}"
  end
end

class B < A
end
A subclassed in B

???

  • Note that it's def self.inherited
    • Because it's a method on the class itself

Method Life-Cycle

  • Module#method_added
  • Module#method_removed
  • Module#method_undefined

???

  • These are on Module
    • Because regular instance methods are defined in modules and classes
  • You will probably find few good reasons to call remove_method or undef_method
  • Removing a method will allow any superclass methods to still be called
    • Only example I can think of is if you dynamically added it
  • Undefining a method will set the method to return a NoMethodError
    • Only example I can think of is if you're trying to look like an older Ruby

Singleton Method Life-Cycle

  • BasicObject#singleton_method_added
  • BasicObject#singleton_method_removed
  • BasicObject#singleton_method_undefined

???

  • But you can also add methods to individual objects
    • These are called singleton methods
    • This is how you define a class method --- it's a singleton method on the class itself
  • These are the equivalent for singleton methods

class: strict_conversion

Implicit (Strict) Conversion Methods

  • to_str
  • to_int
  • to_ary
  • to_hash
  • to_proc
  • to_enum

???

  • These are used to say something "is like" what it's converting to
  • Most of these are used when trying to compare using ==
    • If types are not the same, will try to convert the right side to the same type as the left
  • to_str - used when concatenating strings
    • A UserName class might be a good place to define this
      • Because you want to be able to use it anywhere you use a String
  • to_ary - assigning to multiple left-hand variables (or block arguments)
  • to_int - used when performing bit operations (|, &, ^)
  • to_hash - used to convert method_name(**kwargs)
  • to_proc - called when trying to convert to a block using the & operator
  • to_enum returns an Enumerator, not an Enumerable

class: strict_conversion_2

Implicit Versus Explicit Conversion

  • to_str

  • to_int

  • to_ary

  • to_hash

  • to_proc

  • to_enum

  • to_s

  • to_i

  • to_a

  • to_h

  • &

???

  • Here are the explicit equivalents
  • You almost always want to define the explicit versions
    • Think carefully before defining the implicit variants
  • Important note: to_s is used in interpolation
    • So to_s could be considered implicit in that case
      • Even though it seems implicit to me
  • Important note: *x calls to_a(x) (implicitly)
    • Once again, called an "explicit" conversion
    • I think this changed to to_ary in Ruby 2.0

Numeric#coerce

  • Find a common type for math with mixed types
class Quaternion
  def +(other)
    case other
    when Quaternion
      Quaternion.new(self.i + other.i)
    else
      Quaternion.new(self.i + other)
    end
  end
end
q1 = Quaternion.new(1)
puts 2 + q1
coerce.rb:18:in `+': Quaternion can't be coerced into Fixnum (TypeError)

???

  • coerce helps find a common type when doing math on mixed types
    • Numeric, its subclasses, and similar things like Matrix and Vector
    • Binary operators
  • Looks at self (the left-hand operand) and the other operand
    • Allows you to "cast" the operands into something compatible
  • See http://stackoverflow.com/a/2799899

Numeric#coerce

  • Add coerce to fix the problem:
class Quaternion
  def coerce(other)
    puts "Quaternion#coerce called"
    [Quaternion.new(other), self]
  end
  def to_s
    "Quaternion(#{i})"
  end
end
q1 = Quaternion.new(1)
puts 2 + q1
Quaternion#coerce called
Quaternion(3)

???

  • Note that coerce returns an Array, with self as the 2nd element

Numeric#coerce

  • Coercion transforms binary operators like this:
q1 = Quaternion.new(1)
n = 2
[q2, q1] = q1.coerce(n)
q1 + q2

???

  • This only happens after trying without coercion

Kernel#at_exit

  • Sets a block or proc to run on program exit
  • If $! is set, program is exiting due to an exception
at_exit do
  if $!
    puts "We crashed! #{$!.inspect}"
  else
    puts "fin"
  end
end

???

  • at_exit sets a proc to be run on program exit
  • If called multiple times, they're run in reverse order of registration
  • Can check if $! is set to see if programming has crashed due to an exception
  • NOTE: $! contains the last exception that was raised
    • We can convert it to a string or inspect it
    • If we require "english", it's equivalent to $ERROR_INFO

class: thanks

Thanks

???

  • Thank YOU for coming
  • Thanks to STL Ruby members for feedback on my first version

Feedback

???

  • Please talk to me in the hallway!
    • I'm introverted
    • I live people
    • I like talking to interesting people about interesting things
    • I like talking about interesting things
  • Credits:
    • Clip art from Clipart.co