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!