Talks

A Look at Hooks

A Look at Hooks

by Craig Buchek

In the presentation titled "A Look at Hooks," Craig Buchek explores the use of hooks in Ruby programming. He defines hooks as methods that are implicitly called by Ruby rather than called explicitly by the programmer, which can lead to unexpected behaviors and challenges in debugging. The talk emphasizes the significance of understanding hooks as a powerful aspect of Ruby's meta-programming capabilities. Buchek introduces several key hooks, along with their proper usage, advantages, and potential pitfalls.

Key points discussed include:

  • Definition of Hooks: Hooks are methods implicitly called by Ruby, surprising many developers due to their hidden nature. They can be found in Ruby's documentation under various terms, including callbacks.
  • Common Hooks: The most frequently used hook is 'initialize', which gets called automatically when an object is created. This sets the foundation for using hooks effectively in class definitions.
  • Meta-programming Hooks: The hooks 'methodmissing' and 'respondtomissing' are explored for their roles in meta-programming, allowing the dynamic handling of undefined methods. The importance of using 'respondtomissing' alongside 'methodmissing' is highlighted to maintain clarity about an object's capabilities.
  • Lifecycle Hooks: Hooks related to the lifecycle of methods are discussed, including 'methodadded', 'methodremoved', and 'method_undefined'. These are less common but helpful for managing dynamic method definitions.
  • Conversion Hooks: Implicit conversion methods like 'tostr' and 'toarray' are illustrated, demonstrating how Ruby automatically converts types during operations.
  • Coercion: The concept of coercion in Ruby is explained, detailing how it allows different numerical types to interact, using the example of implementing a 'coerce' method for a custom class.
  • At_exit Hook: Buchek wraps up with the 'at_exit' hook, describing how it allows the registration of cleanup procedures to execute when a program finishes running.

Buchek's presentation stresses the importance of understanding and utilizing hooks effectively while also advising caution and awareness of the complexities they can introduce. Overall, the session provides valuable insights for Ruby developers looking to leverage hooks for enhanced programming capabilities while also addressing best practices to avoid common pitfalls.

00:00:14.540 I think we're going to get started. I'm going to talk about hooks in Ruby today. My name is Craig Buchek, and I have my own consulting firm called Booch Tech. I do a lot of web development, Rails rescue projects, agile player coaching, and some DevOps. I also work with a company called Binary Noggin, and we're going to have a few developers free in January.
00:00:20.740 I do a podcast called This Agile Life, where we talk about agile web development. We emphasize discussing people who work here, not resources. I mentor for a tech institute for underprivileged kids from St. Louis. And apparently, you have to have a picture of your cat, so that's my cat Ivan.
00:01:02.240 So, what is a hook? The definition I'm going to use in this talk is a method that is called implicitly by Ruby, and that we never call ourselves because it's implicit. This can be surprising and difficult to troubleshoot. I like to think of it as 'spooky action at a distance.'.
00:01:19.670 In the Ruby docs, you'll see these hooks referenced in a few different ways. Sometimes they will be called hooks, and sometimes they might be called callbacks. I didn’t want to use the term callback because it has distinct meanings that differ from what we're discussing today. A lot of times, you’ll see the phrase 'Ruby calls this,' which is an indicator that it’s a hook. There are hooks and callbacks in Rails and other libraries, but I'm just going to cover the ones in Ruby today.
00:01:52.540 Who here has used 'initialize'? Probably everyone! Notice that we never explicitly call 'initialize' ourselves, but it gets called anyway. That’s the hook. This is obviously the most commonly used hook in Ruby, but it does define that 'initialize' is found on BasicObject. So, if you are looking up documentation, that’s where you should check.
00:02:14.900 What's happening behind the scenes when we call X.new? Here, X is a class, and when we call X.new, it calls 'new' on the Class class. This seems weird, but it’s how every method call works. You have to figure out that this class is a class of classes.
00:02:22.579 First, we allocate some space in memory to hold the object, and then we take that object and run 'initialize' on it. We should return the object. Meta-programming is where you're most likely to use hooks; the big ones are 'method_missing' and 'respond_to_missing.' How many of you have used 'method_missing'? It seems like a little more than half.
00:02:45.770 'Method_missing' allows calling a method that we never defined. This is probably the first tool you'd reach for when doing meta-programming. Active Record uses 'method_missing' in various places. The biggest downside of 'method_missing' is that you can't search your code for the method you're looking for because it’s not defined there; it’s dynamically found.
00:03:05.440 A couple of things to note about 'method_missing': the method name is passed in as a symbol, and we use 'splat args' as a second argument to 'method_missing' to gather all the arguments into a single array. Splat collects the arguments, and you can also pass a block. I didn’t use the block in this example, but I want to show that that's the signature you should always use for 'method_missing.'
00:03:35.060 'Whenever you use 'method_missing,' you should also use 'respond_to_missing.' Note that while 'method_missing' is on BasicObject, 'respond_to_missing' is on Object. If you’re not subclassing from Object and you’re only subclassing from BasicObject, you don't need to follow that rule. Otherwise, definitely follow that rule.
00:03:59.150 What does 'respond_to_missing' do? If you ask an object if it has a method and that method is not listed in that class or on that object, it’s going to look at 'respond_to_missing.' Before Ruby 9.1.2, we had to override 'respond_to_missing' this way. This way is a lot cleaner.
00:04:20.279 It's also called when you invoke the 'method' method. Note that 'respond_to_missing' can take a second argument. If you pass true, it will look for private methods as well as public methods.
00:04:39.950 You’ve got 'method_missing,' and you’ve also got 'constant_missing,' which is defined on the module. Generally, avoid this one: it seems pretty enticing, but it leads to long-term problems that I don’t necessarily find worth pursuing. I prefer to just use 'require' or maybe use 'autoload.' I don't know where I would quite draw the line; I know Rails likes to use this for autoloading. Remember that class names are constants, so if you're doing an automatic class loading trying to find classes, 'constant_missing' is what you'd be using.
00:05:09.949 The inheritance lifecycle for methods helps us see when some inheritance is happening. The first is 'included.' If you include a module, this will get called. In this example, we are including Module A inside Class B. When that happens, exactly when that happens, A.included gets called with B passed in. You can see what it would output; in this case, it’s 'self.include' since it is on A itself; there’s no instance of A that's being included.
00:06:00.640 Note that we didn't even need to create an instance of B; we just needed to define the class and include it, and it got called. This is useful if you need to keep track of which classes are including a module, and you can provide some way to use that list in some other context.
00:06:31.560 Similarly, if you were extending a class, that's basically the exact same thing. It’s called 'extended' and accepts Module B. It has to be a module because you can't normally have a class extending a module. In Ruby 2.0, we added 'prepend', which is basically similar to 'include' but adds it to the end of the module chain instead of the beginning.
00:06:49.120 We used to have something for meta-programming called 'alias_method_chain.' It seemed like a good idea when we started using it, but it ran into problems; it was very hard to follow the flow. We realized that module inclusion was a better way to do meta-programming in most cases. But 'prepend' works just like 'include', and again we've got 'self.prepended'.
00:07:32.200 Now, this one is a little bit different because it's related to class inheritance instead of modules. There's not even a keyword for that; it’s just Class B < Class A means Class B inherits from Class A. As soon as that happens, you’ve called the 'inherited' method on A, with B passed in.
00:08:07.990 There are some hooks related to method life cycles: we have 'method_added', 'method_removed', and 'method_undefined'. These aren't used too often because we don't dynamically add and remove methods too often. If you use 'method_remove', it leads to the 'method_removed' hook being called, allowing the superclass method to still be called.
00:08:54.200 If you call 'method_undefined' or just call 'undef' on a method, it actually does the same thing as if you wrote a method that just returns or raises NoMethodError. The only reason I can think of doing that is if you’re trying to mimic an older version of Ruby, or if you’re dynamically adding methods and want to start over.
00:09:33.370 You can also add methods to individual objects, which are called singleton methods. A class method is just a singleton method defined on the class itself. There are equivalent lifecycle methods for singleton, so if you add a method to a class or some other object, you would get the singleton method added.
00:10:11.600 The next set of hooks I want to discuss are the implicit or strict conversion methods. We have a couple here: 'to_str', 'to_integer', 'to_array', 'to_hash', 'to_proc', and 'to_num'. These do have equivalents that are explicit, and we'll talk about those in a second.
00:10:44.820 The implicit ones are used when we want to say something is converting to something else. For instance, 'to_str' would be used for something that not only can be shown as a string but should roughly behave like a string. If you create a Username class, you might want to compare it with a string.
00:11:14.780 So if you’re using '==' and the two things are different on either side, Ruby will call 'to_str' on the right side to try to get it on par with the left side. 'to_str' is also used when concatenating strings with the plus sign (+). 'to_ary' is used when assigning to multiple left-hand variables.
00:11:45.420 For example, if you're doing 'a, b = 1, 2', that would use 'to_array' if it needs to do some conversion. We do have equivalents for explicit conversion functions and methods you're probably more familiar with: 'to_s' for strings, 'to_i' for integers, 'to_a' for arrays, and 'to_h' for hashes.
00:12:05.610 Usually, you'd use your explicit conversions in typical cases. However, there are a few places where you wouldn’t; I want to point out that the ampersand (&) is basically syntax to convert to a proc, which calls 'to_proc'.
00:12:26.210 The exceptions here are if you’re doing string interpolation with hashes and curlies, that will actually call 'to_s'. I consider that implicit, but some developers indicate that this syntax is asking for a conversion, so it uses the explicit conversion operators.
00:12:55.299 Similarly, using splat will convert to an array; it will use 'to_a' implicitly. I think this may have changed in Ruby 2.0, but I didn’t get a chance to check that out.
00:13:09.260 This is a case that people often overlook when thinking about hooks. There are several different ways for number-like objects to interact with each other. Ruby's implementation offers a suitable solution, while another is called double dispatch, which is what Smalltalk uses. This is one area where Ruby is quite different from Smalltalk.
00:13:38.900 In our example, we define a mathematical type; it's nothing like a quaternion. However, if you want to define something similar, you might find that it doesn't work because the line of code is attempting to add 'self' to 'other', but it doesn't know how to add 'other'.
00:14:14.590 Ruby kindly provides a hint, saying it can't coerce to 'Fixnum'. This hint urges us to add the 'coerce' function to this class. By implementing 'coerce', we can ensure that the two types can interact correctly.
00:14:48.660 If we do that, we can successfully add the two objects together. What truly occurs under the surface is when 'q1' is a quaternion and 'n' is just an integer. The 'coerce' function will automatically run and set 'q2' to the first coerced item. 'q1' will return itself because we’re returning 'self' and then proceed to execute the addition operation.
00:15:15.560 This tactic works with any binary operator defined on numeric classes. It allows various types—numbers, matrices, and vectors—to interact appropriately.
00:15:43.620 'At_exit' allows you to set a block or proc to be executed when the program exits. If you call 'at_exit' multiple times, the blocks will be executed in reverse order of their registration. You can also check '$!' to see if the program crashed due to an exception.
00:16:18.890 We implement this in the example here, and indeed you can print out the value of it. It’s just a global variable, and if you require the English library, it behaves similarly to 'error_info' for those who aren't fans of Perl-like operators or globals.
00:16:36.670 So, we can convert that to a string. We're inspecting it just like any other object. Thank you for attending! I've wrapped up a little early, so I am open to questions from the audience if you have any.