ActiveRecord

Seeing Metaprogramming and Lambda Function Patterns in Ruby

Seeing Metaprogramming and Lambda Function Patterns in Ruby

by Lukas Nimmo

In the video "Seeing Metaprogramming and Lambda Function Patterns in Ruby," presented by Lukas Nimmo during RubyConf 2016, the speaker explores the vital role of metaprogramming and lambda functions in Ruby, emphasizing their application through real-world examples. Through insights drawn from over 50 prominent Open Source Ruby projects, Lukas outlines ten successful patterns that enhance the design and functionality of Domain-Specific Languages (DSLs) in Ruby.

Key points discussed in the talk include:

- Trust Issues: Developers tend to code defensively due to the potential for errors. Solutions such as using configuration sets allow for organized options while maintaining quality assurance by leveraging tools like the Singleton pattern.

- Situational Language: Context-specific method names enhance usability by providing flexibility without redundant implementations. RSpec's alias matcher demonstrates this by creating synonyms for methods.

- Leveraging APIs: By maintaining the accepted meanings of existing APIs, developers can extend functionalities without introducing confusion. The 'polyglot' library is used as an example of this approach.

- Decorative DSLs: Design DSLs that feel native to Ruby, like ActiveRecord, which automates common patterns while ensuring ease of use.

- Domain Switching: Techniques that allow methods to work across different domains, ensuring intuitive access and execution in various contexts.

- Opening Doorways: Facilitating multi-entry points in libraries, akin to the concept of a house with accessible doors, exemplified by libraries like Clockwork and Faraday for HTTP requests.

- Dynamic Building: Creating objects with flexible interfaces to maintain clarity, using libraries like Jbuilder for seamless JSON object generation.

Lukas concludes that these metaprogramming and lambda function patterns are crucial for reducing complexity and redundancy in Ruby code while enhancing user experiences. Developers are encouraged to apply these patterns naturally as needs arise rather than arbitrarily, facilitating a more organic approach to problem-solving in their development endeavors. The importance of understanding the audience and enhancing the user experience is reiterated as essential for effective library design.

00:00:15.180 Hello everyone, my name is Lukas Nimmo. If you have any comments or questions and you want to contact me afterwards, I had my email up here.
00:00:23.260 I am actually a geologist by education. As a geologist, we study minerals.
00:00:31.210 We study various aspects of geology, and I'm here at RubyConf.
00:00:36.399 Usually, when I meet people, they tend to have this very romantic view of geology, imagining that you're climbing the highest heights and collecting samples along the way.
00:00:45.790 They think of checking out awesome rocks or formations, but in reality, it looks a lot different. It's very exciting, I know, looking under a microscope at the details.
00:01:01.090 In one particular area of geology, you study core samples. Does anybody know what these are? Right, core samples. What you do is study these enough to create a graph or chart that gives you insights into the past environment.
00:01:19.180 Now, you might be wondering, why am I talking about rocks at a Ruby conference? That’s because there’s a nice parallel between programming and geology. If you only look at a small section of a geological chart, just like in Ruby, specifically with metaprogramming and lambda functions, you can't grasp the whole picture.
00:01:38.620 We have many techniques that can be used, but sometimes we get lost in the details, unable to see the bigger picture. Hopefully, I can help clarify that. I want to start off by saying that Ruby is beautiful, and our domain-specific languages (DSLs) should also reflect that beauty.
00:01:51.909 This is not a rip-off of many Swan—though the acronym is quite ugly, I assure you. Ruby is known for being human-readable, and our DSLs should aspire to this reputation, being readable yet powerful. In this talk, we will cover common ways developers use Ruby's dynamic nature to create effective DSLs.
00:02:06.540 I will share insights from many popular libraries and categorize their use of metaprogramming and lambda functions to highlight various use cases and best practices. The libraries you'll see here represent other libraries that utilize similar patterns.
00:02:30.750 When designing interfaces in Ruby, we tend to ask questions like: Who are we designing for? What do our users want from our library? Users want to be able to use the library for its intended purpose and want it to be easy to use.
00:02:47.360 This is not to say that implementers enjoy pain; it’s just the Ruby way of ensuring a positive user experience.
00:03:06.630 Before moving on, I want to discuss painting for a moment. Everyone recognizes the painting in the middle; that's Starry Night by Van Gogh. But would this painting be as famous or influential if Van Gogh had used the style on the left, which resembles Monet, or the style on the right, a 300-million-dollar painting?
00:03:20.520 Probably not. Van Gogh used a very specific style for a specific reason. Throughout this talk, you will see that many techniques and patterns used by different libraries were chosen to achieve a certain aesthetic design. It’s not required; your interface can be ugly if you wish, but that’s not the Ruby way.
00:03:36.790 The roadmap for this talk will involve discussing our goals, problems associated with achieving those goals, examining a public interface that meets those goals, and then diving deeper into the implementation.
00:03:51.010 Our first topic is trust issues. I don't mean to elicit any emotions from you, but developers often face trust issues. We tend to code defensively to prevent both ourselves and others from making mistakes.
00:04:03.780 One way we can do this is through configurations, where we want to provide options while still handling quality assurance. One common approach is using a Singleton pattern. This means that every time a user interacts with an object, it has a global effect, which could create a localized complexity.
00:04:23.495 Let’s look at typical ways to set up an object or library. Each time you interact with a library, there is typically some kind of interaction involved. The simplest way to set it up might involve a configuration object where you create a new instance and explicitly pass in a set of values.
00:04:48.890 However, one problem with this method is that passing in numerous values can be cumbersome to remember, especially as the number of options increases. As a user, I wouldn’t want to constantly flip back and forth through documentation every time I engage with this setup.
00:05:05.590 Additionally, relying on defaults can be problematic because you might forget to pass in a necessary value. A more efficient solution allows you not to provide a value for every single configuration option or worry about the order, but it introduces a risk.
00:05:21.630 Users may set values anywhere they want, which can lead to inconsistencies. So, let’s examine an approach where we can provide configuration options while ensuring quality assurance for the user.
00:05:36.770 Rails offers a solution with configuration sets that organize options for multiple environments. You simply pass a block and evaluate configurations within that block. The magic happens through yielding the self reference.
00:05:52.630 Every time you operate on this variable within the block, you’re essentially working with the Singleton, ensuring that no matter how or when a user sets this up, it will always function around the Singleton to prevent future mistakes.
00:06:11.010 Another method employs a hash, similar to Geocoder, which takes addresses and converts them into longitude and latitude. By passing in a hash, under the hood, it merges those options onto the Singleton, effectively guiding the user towards a specific interface.
00:06:29.430 You might be wondering which structure should be preferred. If both options satisfy our conditions—a block and a hash—many argue that the block is more in tune with the Ruby way of doing things.
00:06:42.360 With the block, every time you operate on that variable, it clearly indicates the action being taken, whereas passing in a hash can seem redundant and may not add to your intention.
00:07:05.220 Now let’s move to case number two, which is situational language. I define situational language as context-specific method names that rely on one implementation. We want a flexible interface for our library, so offering multiple method names that still refer to a single implementation is essential.
00:07:20.290 Here’s a typical way to create flexibility: You define a method called pin_path, which takes a path and appends it to a variable. However, if there are several paths, it makes more sense to pass an array, pushing its contents onto the variable.
00:07:37.540 The issue here isn’t that it doesn’t work; it does, but it can be repetitive. It would be beneficial to achieve both functionalities: flexibility and situational language while relying on one implementation.
00:07:52.530 An application of this can be seen in RSpec and its methods for specifying expectations. For instance, the method find_file allows you to search through a set of paths. Both examples utilize the same underlying implementation, showing how to achieve context-specific flexibility.
00:08:06.339 A familiar pattern involves providing synonyms for methods, which enhances usability. For example, RSpec's alias matcher enables flexibility in documentation, just like methods that allow string inclusion in a given hash.
00:08:25.949 Another common scenario can be seen in verb tenses; for example, using 'satisfy' versus 'satisfied,' depending on the situation. However, one of the drawbacks of these flexible systems is that they might increase cognitive load.
00:08:40.320 Especially for users unfamiliar with a library, the amount of information can feel overwhelming. However, it’s important to recognize that this scope allows them to evolve and become accustomed to the patterns over time.
00:08:57.170 Next up is leveraging APIs. The goal here is to leverage the generally accepted meaning of existing APIs. Whenever you use Ruby, you’re interacting with the public interface created by Matz to solve various problems.
00:09:14.580 Similarly, when you inherit code from elsewhere, you're taking on their design and must determine whether to alter or keep the accepted meanings intact. The 'polyglot' library exemplifies this by enabling clients to load Ruby files with non-Ruby extensions.
00:09:32.230 They implement this by opening the Kernel module and defining a 'polyglot_require' method to wrap around the original 'require' method, maintaining the familiar usage while expanding its capabilities.
00:09:50.370 The design maintains the original meaning of 'require.' This implementation also ensures that users still have access to the original functionality while extending it for modern use cases.
00:10:06.370 However, there are drawbacks. This practice was common in Rails for a time, where for example, 'alias_method_chain' would lead to unintended issues when methods relied too heavily on original implementations.
00:10:22.350 This pattern caused problems when the chained methods no longer maintained proper references, often resulting in infinite recursion. Thankfully, this has been deprecated in favor of more robust solutions.
00:10:38.230 By defining behavior within a module, you leverage Ruby’s object model to create cleaner solutions. For instance, you can rely on a method called 'foo' while maintaining its reliability and manageability.
00:10:57.530 Moving on to decorative DSLs, we want to establish DSLs that feel like native Ruby but serve specific functions in your domain. They should look and operate like Ruby features we use regularly, such as attr_accessor.
00:11:08.240 This functionality automates common class setups while preserving the ease of implementation, providing optional flexibility in designing models.
00:11:24.060 For instance, ActiveRecord can represent a user model. However, your model can define a structure for many objects, from users to books, following a flexible design pattern.
00:11:43.050 ActiveRecord provides a platform to build upon, offering functionality when interacting with the model. The 'has_many' method is one such example, creating a simple yet powerful descriptive statement.
00:12:01.310 While there is complexity beneath the surface, the implementation feels natural for working with databases, reflecting the relationships between different models.
00:12:15.519 For case number five, domain switching involves defining behavior in familiar domains, allowing execution elsewhere. For example, consider Sinatra where helper methods are accessible in a way that feels intuitive.
00:12:29.940 Helper methods could be defined at a top-level namespace, enabling easy access in views, enhancing user-friendly interaction. The design allows you to define methods that can be called in various contexts.
00:12:45.040 Every time you invoke a method at the top level, it gets routed to the relevant context, executed appropriately through blocks that evaluate within necessary contexts.
00:12:55.740 Our final focus is on opening doorways. We want users to have multiple entrances into libraries similar to that of a house. You wouldn’t place a door on the second story if access requires a ladder.
00:13:09.960 Instead, we seek to create convenient entry points in a well-structured framework. For example, Clockwork executes applications at specific times and provides accessible management options.
00:13:22.920 The goal is to allow easy access without convoluted routing through various classes. You wish for direct access to the manager without unnecessary complexity.
00:13:40.370 Faraday serves as an HTTP client library, which means users will frequently make GET requests. We want to ensure they can do that efficiently without needing intricate class scoping.
00:13:59.420 There's a method-missing pattern in Faraday that enables creating requests without repetitively establishing new connections, making the user experience seamless and straightforward.
00:14:15.990 Case number seven revolves around dynamic building. We want to create objects that provide a broad interface, reducing redundancy within our code.
00:14:30.670 Using libraries like Jbuilder, you can build JSON objects dynamically, allowing you to pass attributes fluidly without repetitive method calls, further maintaining elegance.
00:14:46.500 We want to mimic Ruby methods, where objects can be dynamically built without verbosity, showcasing the simplicity that Ruby prides itself on.
00:15:04.090 With patterns showcasing clarity while promoting flexibility and expressiveness, it’s evident that we’re leveraging Ruby’s existing structure to our advantage.
00:15:15.510 To summarize, we’ve examined numerous metaprogramming and lambda function patterns in Ruby. These patterns do not exist in isolation—they're utilized in real-world libraries.
00:15:27.850 As we conclude, reflect on how these patterns not only enhance code but also streamline user experience, reducing unnecessary complexity and redundancy.
00:15:42.070 It's essential not to arbitrarily decide to apply a specific pattern, but instead, let the need arise naturally, finding solutions to challenges organically.
00:15:58.040 Remember, you're more likely to retain the information shared early in a discussion and the important takeaways at the end, like situational language.
00:16:12.100 Start by recognizing trust issues when designing global interfaces. Situational language needs arise from creating flexibility, while leveraging existing APIs emphasizes understanding existing contracts.
00:16:30.870 Decorative DSLs offer predefined functionality, allowing users to declare their needs. Domain switching allows for familiar functional use across different execution domains.
00:16:45.990 Opening doorways promotes seamless access to nested structures, while dynamic building enables creating interfaces with minimal redundancy.
00:17:00.730 In conclusion, understand these patterns will serve you well in future development. Questions?