00:00:07.950
Hello! Today I'm going to talk to you about keyword arguments in Ruby. We'll see why they're useful, how they were introduced gradually in Ruby over the versions, and how it all starts with the options hash.
00:00:10.990
You may already have a good idea of what keyword arguments are, but let's start with the definition. Wikipedia states that keyword arguments refer to a programming language's support for function calls that clearly state the name of each parameter within the function call itself.
00:00:18.369
In Ruby, when we call a method, we name each parameter as we pass it an argument. Let's first look at the advantages this brings over regular parameters. When we define a method in Ruby, we give it an ordered list of parameters. This means that later, when we're going to call the method, the position of the arguments will matter, which is why they are called positional arguments.
00:00:54.280
When you look at the method call itself, there’s no way to guess which arguments correspond to which parameters. If the method call is in a different file than the method definition, which happens often, this can lead to confusion. If we examine the definition of a method with keyword arguments, we see that it's very similar.
00:01:20.590
However, when we read the method call now, it’s much clearer what we’re going to get. Also, the order of the arguments doesn’t matter. So, if you're a bit particular about order, like me, you can arrange them alphabetically. We don't need to know the order of the parameters anymore.
00:01:48.490
Of course, we need to know the names now, and we need to type them. They take up space on our screens, but it’s more verbose this way. However, I would argue it’s also more maintainable because it allows you to change things more easily.
00:02:05.289
The only thing we can be certain of is that there will be changes later on. Thus, it’s easier to add new parameters or replace existing ones. For example, if you want to support percentage taxes, it will be much easier than using positional arguments.
00:02:45.220
This phenomenon can be referred to as coupling—basically a design pattern in object-oriented programming. It means that if two things are coupled, changing one will necessitate changes in the other, or at least you might need to check the corresponding call sites. In our case, if we change the positional method definition, we will likely need to modify the method calls in other files.
00:03:15.760
So, I won’t spend more time on coupling, but if you want to learn more about it, there are great resources available. At this point, we might think it would be a good idea to use keyword arguments everywhere. But, obviously, there’s always a trade-off.
00:03:50.480
If you have a method that only takes one parameter—or even two parameters if they’re obvious—using keyword arguments won’t provide useful clarity. Instead, it can make your code more verbose and, potentially, more confusing. For example, in complex numbers, the definitions I've read typically specify the real part followed by the imaginary part; this is clear enough and doesn’t require naming them.
00:04:26.110
The same principle applies for rational numbers, which are defined by the quotient and the denominator. Similarly, when defining methods that take a number of elements in a collection, it’s not necessary—let alone helpful—to name the parameter 'n' or 'count.' In those cases, it just increases verbosity.
00:04:57.740
Interestingly, you might often want to use literals for imaginary and rational numbers, so it’s best to keep things simple. Now, the first thing you should know about named parameters in any programming language is that you can emulate them using any data structure, such as an associative array like a struct in C, an object in JavaScript, or a hash in Ruby.
00:05:50.500
And it makes sense because an associative array allows us to associate a name with a value, which is similar to what we want to achieve with keyword arguments. You can think of this as comparing a two-dimensional array to a simple one-dimensional array that reflects a regular parameter list.
00:06:00.960
For a long time in Ruby, we’ve used what we call an options hash, particularly for optional parameters. By passing a hash, we effectively name the parameter, and we can also provide the value directly. The great thing we all know and love in Ruby is that we don’t need the implicit hash—we can omit the curly braces, which looks pretty great.
00:06:25.960
In fact, while doing research for this talk, I found a reference in the first edition of Programming Ruby, which came out when Ruby 1.6 did not support keyword arguments. It even mentioned the Ruby schedule, which made me giggle. Now, moving back to our example, if we wanted to add a percentage tax feature, we could simply add default values.
00:06:56.070
However, the problem arises when the full hash is treated as a single parameter, preventing us from acquiring the default tax rate value. Over the years, several techniques emerged to get around this with boilerplate code, and various libraries provided ways to express this more clearly.
00:07:28.490
For example, Active Support offers reverse merge, which makes the code easier to read at a glance. Nonetheless, it’s still something we would prefer not to deal with. The options hash has become a popular pattern within Ruby, and we use it often. In fact, if we look at Rails, in the latest version, it has been used almost 500 times throughout the entire codebase.
00:08:02.789
In Shopify, it’s even more widespread! So, let’s return to keyword arguments and explore how we can trick Ruby into using the options hash as keyword arguments. One straightforward method involves simply using the values.
00:08:34.690
Using hash indexing can seem appealing, but it lacks clarity. What could possibly go wrong here? For instance, let's say we forget an argument; it leads to a 'no method' error. In such cases, it’s quite easy to see the issue, but in other cases, it can become buried within a deeper stack trace.
00:09:06.540
What we've learned, especially from instructors like Fred George, is that we should be using fetching for parameters to clarify that these keys are required. Yet, even then, things can go wrong. For instance, we all have those mornings when we're coding before our first cup of coffee, leading to unexpected results stemming from typos.
00:09:45.450
To help catch these potential issues, Active Support provides a method called 'valid keys,' which assists in validating the expected keys. Another issue arises when we try to combine the use of the splat operator with options hashes.
00:10:26.289
This situation requires additional boilerplate code to handle the merging properly. Throughout this talk, I’ve come across multiple instances where developers thought they were just one line of code away from a solution only to find it was going to take much more.
00:10:55.790
Indeed, in Rails, they implemented extractable options to function exclusively with instances of hash. Over time, they’ve had to redefine it for other subclasses, allowing those specific classes to utilize extractable options, primarily due to issues with overlapping definitions.
00:11:18.129
One of the problems that developers hope to address concerns the parsing. We wanted this functionality to seamlessly integrate into Ruby, creating a consistent behavior for everyone. Returning back to Ruby 2.0, we used the newer hash syntax with a feature that is visually appealing.
00:11:50.250
It's much nicer around the eyes, yet it did not change the methodology of the method definition itself—only the method call changed. This syntax works exclusively when the keys are symbols, but typically that’s what we desire.
00:12:22.860
However, sometimes this is not the case. Ruby 2.0 introduced optional keyword arguments, which was a monumental improvement for us. We could remove a considerable amount of boilerplate code, ensuring that all optional parameters were defined, reducing the likelihood of errors due to typos.
00:12:53.610
With Ruby 2.0, the double splat operator was introduced, collecting all remaining keyword arguments into a hash, reverting us to a previous state. Yet, we still needed to use fetch to prevent careless mistakes. Ultimately, Ruby 2.0 led to the introduction of required keyword arguments.
00:13:27.190
This addition was met with great enthusiasm because we were finally able to use keyword arguments seamlessly. Initially, I experienced some confusion regarding behavior when I first attempted to implement keyword arguments, which felt somewhat odd, almost like a bug. In scenarios where a method takes no parameters, calling it without providing keyword arguments works.
00:14:02.329
However, if we pass no keyword arguments at all, the resulting behavior can be unexpected.
00:14:35.950
This inconsistency persisted until Ruby 2.2, which streamlined the process, allowing it to function correctly. There was a crash in the parser during development, but thankfully, that crash was never deployed and was identified. The fix introduced a peculiar side effect where it now ignores empty literal hashes.
00:15:14.310
Nonetheless, you can still achieve unexpected behaviors just by passing a value. The core issue here underlines my argument that keyword parameters don't truly exist in Ruby; they aren’t regular parameters available for access and manipulation by name.
00:15:46.469
Despite keyword arguments improving significantly over the years, they still don’t feel entirely material. To illustrate, let's compare Ruby with another language like Python, which has a more comprehensive implementation of keyword arguments.
00:16:19.919
In Python, any parameter can be named without needing a special definition. The parameters inherently possess both name and position, allowing you to pass any number of parameters positionally until you choose to pass them by name.
00:16:54.179
This applies even to optional parameters, which can still be passed on just like how we do in Ruby. Revisiting the Wikipedia definition, we remember it references function calls. However, in Ruby, we’d prefer to say we’re sending a message.
00:17:22.999
When we send a message in Ruby, we pass arguments and potentially a block of code. Moreover, the arguments are just an array—we don't actually pass keyword documents.
00:17:54.880
It turns out that arguments are similar enough to an array that we can utilize the same syntax when sending messages. The simplest example involves the splat operator, which allows for a similar context format. Notably, we can also use the hash collection feature with an array literal in the same way.
00:18:23.860
This is why I argue that when sending messages, we are essentially sending an array of arguments, and the method call syntax merely employs array literal syntax. At this point, we revisit the use cases of passing any number of arguments, leveraging options and the splat operator.
00:18:56.700
The implementation of the double splat operator in Ruby 2.0 had some oddities, where the placement of the double splat would affect the behavior in unexpected ways. Ultimately, within Ruby 2.1, the method calls and definitions started having clearer defaults.
00:19:34.050
If we examine the usage of double splatted hashes, the precedence rules dictate that external hash values take priority over those within the double splat.
00:19:57.560
Going forward, if we want to override a literal key with an option hash, we can achieve this by splitting the hashes after the original method call.
00:20:07.200
Ruby 2.2 allows us to compactly send merged hashes. This brings us to the implicit conversion, which is one of Ruby's exemplary features.
00:20:42.720
Duck typing is applied effectively here, as you may remember with the symbol to proc method. However, if you pass any object and attempt a double splat, Ruby will block this process if an implicit conversion isn't defined. The longer name methods present here can lead to confusing behaviors as well.
00:21:18.900
The implicit conversion to hash may be overly generic and conflicts can arise when swift methods are used for merging without realizing the expectations of key types within the double splat context.
00:21:55.900
To conclude, what remains open for discussion is the best approach for explicit conversions since we are already writing conversion routines within methods to get values from objects directly.
00:22:24.780
Now let's shift our focus to performance. Although it’s not the paramount concern, it does carry weight in Ruby 2.1. Calling methods with keyword arguments can be notably slower than expected due to its internal handling.
00:22:59.349
For instance, executing 10 million method calls on a simple hash may take around one second compared to the significantly slower performance of keyword arguments. However, Ruby eventually improved the performance by enhancing how method definitions interact with the virtual machine.
00:23:37.809
You can see this process by checking out the generated bytecode from a Ruby method call. In this context, the first step involves defining the method. The second part consists of the compiled method output.
00:24:10.280
The internals of method traces allow Ruby to comprehend method entry and exit, which greatly assists in performance metrics.
00:24:41.739
By utilizing bytecode analysis and the virtual machine, keyword arguments' performance improved significantly in Ruby 2.2, where the arguments would be stacked as standard parameters and the method effectively behaved as we would anticipate.
00:25:09.020
In this setup, the method would naturally declare the keywords rather than checking for their existence during each call.
00:25:42.780
Ruby 2.2 also improved the hash syntax by allowing keys that wouldn’t traditionally be identifiers, enabling interoperability between JSON structures and Ruby hashes.
00:26:19.200
Indeed, JSON structures now work seamlessly in IRB when properly defined, which leads to more efficient and readable code.
00:26:45.070
Nonetheless, a repeated concern emerges when dealing with keyword arguments: they can become verbose. In many cases, like in factory methods, one could potentially simplify code by passing a varying number of keyword arguments, which would seamlessly relay to the new method implementation.
00:27:09.950
As keyword arguments proliferate, their redundancy becomes noticeable, leading to contemplation of improved syntactic aids akin to ES6 syntax, allowing for more compact argument usage.
00:27:34.630
Typically, upon returning to the original issues with keyword arguments in Ruby, we can illustrate the issues faced. When observing instances where no keyword arguments are passed for dispatching, one risks failure due to methods not designed to handle parameters.
00:28:09.040
Currently, the workaround involves using boilerplate code to avoid pushing empty hashes into methods that can’t accept them. In conclusion, it’s apparent that many challenges associated with keyword arguments have already been addressed collaboratively by the Ruby core team and the community.
00:28:48.080
Moving forward, we can ensure that keyword arguments always operate fruitfully within the Ruby framework—and perhaps evolving the semantics around message passing could prove beneficial. Feel free to connect with me at a Ruby event or via Shopify, and I am happy to support with any questions you might have.