Talks

Promises in Ruby

Promises in Ruby

by Dinshaw Gobhai

Promises in Ruby

In the video "Promises in Ruby," Dinshaw Gobhai discusses how to effectively utilize promises in Ruby to manage complex workflows. The talk, presented at RubyConf 2014, explores the challenges of traditional sequential workflows and shares insights into implementing promises as a cleaner, more manageable coding pattern.

Key Points Discussed:

  • Introduction to Promises: Gobhai introduces himself and explains his background with Ruby since 2000. He shares his experiences with automating ad publishing workflows that often involve lengthy sequential processes.

  • Challenges with Traditional Patterns: He highlights the difficulty of managing long top-down workflows and how using state machines can complicate exception handling, leading to unmanageable code.

  • Exploration of JavaScript Promises: The presenter reflects on a friend's suggestion to explore JavaScript promises, leading him to realize their potential in Ruby after further investigation.

  • What Are Promises? He describes promises as a method to establish direct correspondence between synchronous and asynchronous operations, making code more natural and readable compared to nested callbacks.

  • State Management: A promise transitions between three states: pending, fulfilled, and rejected. This structure allows for better error management, as it defines clear pathways for success and failure through chained calls to 'then'.

  • Implementation in Ruby: Gobhai illustrates how the promise pattern can be adapted in Ruby, showcasing examples from his codebase. He emphasizes the importance of using closures (lambdas) within promise implementations.

  • Asynchronous Operations: Gobhai discusses how promises allow for non-blocking operations, enabling developers to handle multiple tasks simultaneously. He presents examples demonstrating 'Promise.all' and 'Promise.any', showcasing how to manage collections of promises effectively.

  • Benefits for Code Readability and Maintainability: The video emphasizes how adopting promises can improve the structure of code, making it easier to follow and refactor.

  • Reflection and Future Directions: Gobhai acknowledges challenges that remain in operationalizing promises in production environments and encourages developers to continue exploring best practices for implementing promises in their work.

Conclusions:

Gobhai concludes by highlighting the elegance and utility of promises in Ruby, advocating for their adoption to enhance software development practices. He expresses gratitude to individuals who assisted him in understanding and presenting these concepts, reinforcing the community aspect of learning in programming.

Overall, the talk aims to inspire Ruby developers to embrace promises to simplify asynchronous programming and improve workflow management.

Happy coding!

00:00:18.160 Good morning, and thank you for coming to 'Promises in Ruby.' I'm super excited to be here, sharing what I believe is some pretty cool code.
00:00:23.199 My name is Dinshaw. I've been doing Ruby since about 2000. No, sorry, I've been building websites since about 2000 and found Ruby on Rails in 2006. I now work at Constant Contact in one of our New York offices in Tribeca.
00:00:35.440 Recently, we've been trying to automate the publishing of ads to various ad distribution services. These services tend to have complex domains and expose them via RESTful services.
00:00:47.280 What this means for us is that we've had to manage these really long sequential workflows. We create one object on the remote service, receive an ID back, and then use that ID to create the next object. We do this multiple times, and then we have to send all the IDs back to associate them with each other. We started off coding this from a top-down approach, and it quickly became ugly.
00:01:16.320 Coming from an object-oriented world, we didn’t want to see a long list of top-down methods. We attempted to improve our situation by using a state machine to manage the workflow, but many of you may know that trying to use a state machine for complex workflows quickly becomes unmanageable.
00:01:44.799 You often find yourself bouncing between the callbacks you've defined in your state machine and the actual code you’re running, leading to confusion about where to handle exceptions. Nothing made sense, so we started looking for a better approach that reflected how we thought about the problem in code. A friend of mine suggested looking at JavaScript promises, which I found interesting.
00:02:02.399 I did a superficial implementation in Ruby, and we used it for a while. Then, when this talk got accepted, I revisited promises and realized that I had completely missed the point of promises in Ruby. After a deeper dive, I discovered that it’s just a really cool pattern, and watching it happen in Ruby is exciting.
00:02:20.000 We'll be looking at a lot of code today as we go through the implementation we're currently using. The slides contain links to additional reading and resources, including my GitHub page. I’ve also tweeted both the slides and the code base. Feel free to reach out if you're interested in checking them out.
00:02:39.280 So, what are promises? Where did they come from? I'm quoting here: 'Promises are trying to provide direct correspondence between synchronous and asynchronous functions.' This quote is from Dominic Denicola, who could be considered the modern father of promises, at least in the context of JavaScript.
00:02:50.000 The first link here is a presentation he gives called 'Callbacks, Promises, and Co-routines,' where he discusses their origins in languages designed for managing distributed systems. These languages heavily focus on concurrency. In his blog post titled 'You're Missing the Point of Promises,' he clarifies the core concepts behind promises, noting that early implementations were often disparate, as people picked and chose parts of the spec to implement.
00:04:10.400 Much community work and contributions culminated in the Promises A+ specification. If you read it, you’ll find names familiar to both the Ruby and JavaScript communities among the contributors.
00:04:35.280 To understand what a promise is, we need to start with the problem that promises try to address. If you've written any JavaScript, you've probably encountered code like the following. We're assuming 'getCat' is a function that makes an HTTP call to get a representation of a cat. Because we can't predict how long this call will take, we pass in a function and gain access to either an error or the cat, depending on the outcome.
00:05:01.039 We check for an error; if it exists, we handle it. Otherwise, we proceed. This pattern is manageable and readable, but it doesn't resemble the way we naturally think about the problem. Wouldn't it be nice to express our intent, like wanting to get a cat and its picture, without focusing on the implementation details?
00:05:25.600 Promises attempt to solve this problem by allowing us to chain calls and structure our code similarly to our thought process. An example of working promise code shows that 'getCat' should return a promise. One of the contractual obligations of a promise is that it must respond to the 'then' method.
00:05:50.960 A promise operates in one of three states: it begins in a pending state and can become either fulfilled or rejected. Upon fulfillment, it follows the success path through all subsequent 'then' calls. If any operation fails due to an exception raised or a designer's decision to mark it as unfulfilled, it switches to the error path, which allows for central error handling.
00:06:09.280 What does this look like in Ruby? Here’s an example of what we faced in Ruby: we dealt with huge methods containing numerous functions, each returning an ID that needed to be passed into the next function, all happening sequentially. Even though this example is short, imagine it with 15 or 20 lines, which makes error handling and concurrent execution challenging.
00:06:41.520 We needed a more powerful solution. So, how would the promise pattern look in Ruby? It would mirror the pattern in JavaScript. The promises specification is just that—a specification, not a coding library—so it can be implemented in any language. As long as you adhere to the rules, it will be structurally similar.
00:07:06.639 The assumptions we're making in this code include that 'getCat' is a function getting a cat from a remote service, returning a promise that responds to it. It's important to note that the arguments to 'then' are lambdas, which are closures—self-contained pieces of code that include their scope. If you're unclear about the difference between blocks, procs, and lambdas, I recommend reading an article by Adam Waxman.
00:07:16.719 In this presentation, we will use lambdas consistently as self-contained pieces of code that can be invoked with '.call.' So, the 'getCat' method returns a fulfilled promise, and the chain follows through to 'getCatPicks' and 'showCatPic.' The arguments of 'then' dictate which lambda gets executed based on the promise's status.
00:07:39.600 As we peel back the implementation details of 'getCat,' we will work with a lightweight promise that utilizes a convenience class method called 'promise.start.' This helper instantiates a new promise and fulfills it immediately, setting the argument passed in as its value. We assume our cat class has a class method called 'get' that retrieves a cat representation. The promise starts, fulfilling its value and moving to line two, where it runs 'getCatPicks.' If successful, it will show the cat picture.
00:08:09.680 To illustrate, we’ll demonstrate 'promise.start.' In real life, the operation will return a promise in the fulfilled state with the value 'Garfield.' The pending steps are not the focus now; we will delve into them shortly. The next example shows how to demonstrate the 'then' part. We maintain our same exact 'promise.start.' It will fulfill, passing itself to '.then,' executing the left-hand argument—our success lambda, where we simply output its value. The error lambda is optional; if omitted, it passes through to the next then until an appropriate action can be taken.
00:09:03.360 Now, we will create a more complex promise. We won't output it but will instead manipulate the string value as it passes through the thens. It will yield 'Garfield. The cat is lazy.' One of the benefits of promises is their ability to extend beyond their lifecycle. If the promise is still running, it does nothing until it is fulfilled or rejected. If it has already been resolved, it immediately runs the corresponding code.
00:09:39.840 What happens when something goes wrong? Initially, we have a fulfilled promise with a value of 'Garfield.' We pass to a success lambda that will raise an error. When it reaches the error handling and receives an alert, it shows 'Garfield sucks.' We're starting to see the power of managing workflows and exceptions throughout the code.
00:10:03.360 The value, as noted at the beginning, provides correspondence between synchronous and asynchronous actions. Let’s review the 'start' method, which is a convenience method that only returns a fulfilled promise with a value of 'Garfield.' Here’s the implementation of that method, which we’ll revisit at the end when we check the complete source.
00:10:40.960 The start method initiates a new promise, exposing fulfill and reject methods within the code block. It’s a powerful Ruby feature I had never used before. The block of code passed into this promise holds everything it needs to fulfill or reject this specific promise instance, setting it to fulfilled and assigning the value of 'Garfield' by calling the fulfill lambda.
00:11:05.360 Let’s take a closer look. This simplistic example of an instantiated promise has condition-based decision-making for either fulfilling or rejecting the promise. We’ll try to get our cat picture if we have one; if successful, we call fulfill and continue down the success path. If not, we can reject it and move into the next sequence of events.
00:11:29.440 The actual initialization code shows the yield functionality and its benefits. We set the state, value, and invoke the fulfill and reject methods. Using 'yield,' you can execute any code passed to any method in Ruby, only executing it if there is a yield present. Here, we’re providing lambdas containing the implementation of fulfill and reject to the block of code instantiating the promise.
00:12:27.520 More code examples: You may have noticed a false in the constructor indicating synchronous operation. The default executes code you supply asynchronously, enabled by threads. We will review this code shortly. When execution happens asynchronously, we receive a pending promise promising the value of 'Garfield.' This is crucial because the promise should return immediately, allowing further interaction with the workflow while waiting for resolution.
00:13:02.639 This pattern remains important; the promise returns immediately and alters instance states, allowing for background processing. If 'Garfield' were out fetching cats longer, we could manage fulfillment in the meantime, maintaining a handle on the promise and the state transition.
00:13:59.679 At this point, let’s run a more extended asynchronous operation. Each promise, sleeping for random durations, will yield results. As shown, we observe outputs reflecting their respective sleep durations: 'Garfield slept 7 seconds, Felix slept 8 seconds.' These promises should fulfill individually and allow the program to track their state. Suppose you want to run something collectively—then we can utilize 'Promise.all' to ensure all promises complete before moving ahead. This feature is incredibly useful for asynchronous operations, returning results when all tasks complete.
00:14:54.959 With 'Promise.all,' we define multiple promises sleeping for various times, tracking their outputs as per their completion. This alludes to the asynchronous nature of promises. The code runs, and each promise resolves individually. The then of 'Promise.all' executes once all have fulfilled—informing that everyone is awake. Equipped with a handle on these promises, you can observe and initiate further actions.
00:15:57.440 Moving on to 'Promise.any,' we can watch which promise wakes first. If all promises are resolving but only one is needed, then once any of them resolves, we fulfill the overall promise. If we define all but one to fulfill or reject by the result, this showcases the clear distinction between acceptance and rejection states. Each promise runs concurrent executions that ultimately connect back to the surrounding process, ensuring that focus remains clear as we identify individual promise states.
00:16:31.679 In implementing 'Promise.any,' we loop through all promises and handle every success or rejection accordingly, granting flexibility in execution outcomes and understanding how to react to their specific results—this ‘race’ implies that whichever is first to run results in fulfillment while others may take longer to fulfill or reject.
00:17:40.319 So, summarizing, my colleague Mike Davis emphasizes this approach: wouldn’t it be great to visualize these flows the way we think of them? That’s crucial when refactoring code; no one enjoys returning to difficult sections that create dread or embarrassment while trying to improve them. Seeing well-structured code, however, creates motivation. Yet, time often leads to such structures degrading when other complexities merge, risking convoluted logic. These elegant patterns are crucial to keeping code readable and maintainable.
00:18:48.360 To consider key concepts of promises: they should return control immediately, fulfill or reject, provide an easily chainable response to 'then,' and execute no more than once. The state transitions are critically important, knowing that a promise set in immutable states is finished while ensuring compliance with these requirements and the assurance of 'then' usage.
00:19:29.040 Let’s examine the source code in detail; I’ll start there to clarify elements that might be vague. We’ll review the complete functionality and a couple of convenience methods for fulfilling and rejecting states. Diving into the initializers shows us how to set up a promise and use fulfill or reject effectively. The simplicity allows for asynchronous tasks to fulfill contracts directly, using concurrent methods for outputs. The current state keeps track of values throughout to ensure the most accurate transition across each stage.
00:20:45.600 Moving into resolve, we will determine if a promise state is fulfilled or rejected with careful observation of the existing state throughout the promise's lifecycle. Once resolved, it executes the desired lambda, whether it is a success or an error path, allowing continued progress through the promise chain. The resulting values represent the collective output based on previous transitions, offering transparency in determining how data flows through asynchronous executions.
00:22:21.440 In this conversation, it may seem convoluted, but understanding promises through examples illustrates their versatility while illustrating their threading concerns in Ruby. Thread safety was a phenomenon we cautiously observed through context operations, and existing libraries provide structure to alleviate the ambiguities in an environment designed for concurrency. In some cases, more experience should yield insight, and continual practice helps command efficiency in usage and understanding of these patterns.
00:23:40.760 This feedback loop is key in developing promise implementations; as libraries mature, they learn from how to handle returns, recognizing potential complexities. The conversation here suggests promise structures pave a path toward much safer executions than novice efforts. For those considering a production using this pattern, following through on detailed implementation is essential, and the transition might foster a blend that successfully executes.
00:24:59.680 While the implementation is still evolving, challenges remain in operationalizing these ideas. We are not deploying promises in production yet; rather, we explore their potential in an academically rigorous context that fosters understanding. For those eager to apply these principles: study their implementations and share thoughts. Our team is currently engaging in practical exercises fostering these discussions with groups in our current work.
00:26:17.120 I want to acknowledge thanks to a few key individuals: Brian Mitchell, a friend who helped clarify these concepts for me; Mike Davis, who assisted with the writing process of this talk; and Jed, a coworker who created a wonderful structure for these presentations using simple markdown reflections. As we draw to a close, are there questions or comments on any topics covered today?
00:27:34.320 One question asked about testing the asynchronous features—it's tricky but achievable. Asynchronous layers complicate assertions, so some pragmatic solutions exist, but the overall implementation reveals potential areas for clarification and better practices. Rest assured, this promise construct could evolve to address testing more effectively, and feedback during usage groups helps refine these operational models. Valuable insights emerge to foster these discussions, transforming concepts into tractable workflows continually evolving.
00:29:11.200 Exploring implementation depths anchors understanding within structured programming, weighty tasks. Further reflections evolve the discussion, interlacing theoretical knowledge and practical applications for everyday situations. Where are the bumps—current particularly critical points currently in your software context? This reinforces design patterns to remain vigilant while streamlining promises into carry-forward communication, task handovers illustrating that even through uncertainties, we can chart a path forward that cites elegance and utility.
00:30:57.919 Ultimately, I believe this pattern can ultimately assist numerous projects and models, fostering more significant clarity, efficiency, and structural integrity. Your efforts in refining how to utilize asynchronous operations will undoubtedly contribute toward improving software development practices.
00:31:40.799 Thank you all very much for listening, and happy RubyConf!