Piotr Szmielew

Bindings in Ruby - Behind the Magic of Blocks

wroc_love.rb 2017

00:00:11.810 Okay, hi! My name is Katrina. I have been working with Ruby on Rails since 2009. I work at the Wrocław University of Life Sciences as an assistant and also mentor in Qatar.
00:00:25.050 I want to ask you a question at the beginning: how many of you have ever used the Binding class? Okay, how many of you have ever used blocks in Ruby? Yes, everyone! This riddle will be solved in a few minutes, but let's start with a quite simple example. What will be the output of this program? There is no trick here; Ruby will just be Ruby. Hopefully, the result will be 12.
00:00:42.360 It seems obvious, but let’s make it a little bit harder and consider this method. You can use Unicode in methods, as I discovered today! It's kind of fun. What will be the output of this? It's not really obvious at first glance, but will it work? Yes, it will work, although it’s kind of unusual.
00:01:04.710 To understand this, we need to answer a question: What is actually a block in Ruby? A block in Ruby consists of two parts: the code (which I mean in relation to lambdas in computer science) and the environment. Traditionally, it's called a closure. So, what is actually a closure? The first part is the code. As you saw before in the presentation about Ruby, you can disassemble the code of compiled Ruby because it compiles to assembler instructions.
00:01:43.140 When you do this, you get a long output. But, there are two interesting things here. When we call a block, we actually set local variables, which is really unusual because they are local in the block but can be accessed outside of it.
00:02:03.060 This leads us to understand closures better. The second part of the definition includes the hidden aspect that a closure also contains its environment. Let’s think about what this environment is. The Ruby virtual machine (MRI) runs our code, and it actually employs a virtual machine with a stack and a heap. The current execution state is contained in the stack frame, hence local variables are stored there.
00:02:59.310 When we use lambdas or compile Ruby instructions, we change the block into a first-class citizen object like a normal variable. However, there is a catch: blocks are closures, which we already know, and closures need their environments. Therefore, we need a way to encapsulate the environment of code inside our block.
00:03:58.839 This brings us to the hidden hero of this presentation: the Binding class. From a Ruby perspective, it's a simple object; it has instances of the Binding class, and it inherits from Object. So, nothing very interesting here. Binding is created simultaneously with lambdas.
00:04:17.250 We can also create it manually. Now, when we consider creating this binding with a lambda or block, we may ask ourselves: How is a binding actually created? What’s happening under the hood? We’ll start with the MRI version because it’s the most commonly used Ruby interpreter.
00:05:06.030 The binding method, which returns binding, is encoded in a proxy file that is responsible for the colonel binding method. This means that it's calling some C functions. Let’s dig a little deeper.
00:05:26.940 We see that it is responsible for our VM to create bindings and that there is actual code being called when you retrieve the binding. We can see local variables contained in some way here.
00:06:02.700 The actual magic happens in the vm.c file, which we will look into now. The function called vm_make_binding also calls other functions recursively. By observing the vm_make_binding, you can see that local variables on the stack frame are in fact copied from one area of memory to another.
00:06:58.290 So, when we call binding in MRI, we actually invoke a chain of functions that ultimately copy local variables from the stack elsewhere.
00:07:26.490 Now, let's explore RubyNu. How many of you have used RubyNu commercially? Quite a few! RubyNu is an interesting and ambitious project that aims to translate and write an interpreter for Ruby itself in Ruby. The good thing about this is that when you write in RubyNu, you have all the libraries, like enumerables and string manipulation, written in pure Ruby.
00:07:59.590 This makes it easier to understand. Of course, it is not practical to write a complete Ruby interpreter in Ruby alone, so they utilize the LLVM compiler.
00:08:26.580 Moreover, while analyzing the binding method written in Ruby, we can see that it calls Ruby's instance variable scopes. When we encounter RubyNu's primitives, we transition from Ruby code into C++, which can be quite bewildering.
00:08:58.300 Now, looking into the C++ code, we find an interesting method that promotes the scope. If local variables are already on the heap, the function does nothing, but if they are not, they will be created on the heap, allowing methods to access them as before.
00:09:13.000 When calling the binding method, variables are copied from the stack to the heap, creating a new scope that will point to the heap variables. In summary, when calling the binding method, we essentially copy local variables from the current execution context.
00:09:38.840 When you involve lambdas or procs, a similar process occurs where locals get transferred from stack frames to the heap.
00:09:55.590 Let's take a small break from MRI and consider JRuby. How many of you use JRuby commercially? There are a few hands. JRuby is an implementation of Ruby written in Java that can run on the Java Virtual Machine.
00:10:12.080 The good thing about JRuby being written in Java is that it’s often easier to read. While you may have different experiences, I believe JRuby has two classes: Binding and Ruby Binding. It can be a little misleading since Ruby Binding is the class we deal with when we get the binding method.
00:10:43.700 On the other hand, Binding internally represents a block, pointing back to Ruby Binding, which is not directly exposed in the JRuby interface.
00:11:16.160 Interestingly, Java stores its objects on the heap rather than the stack, meaning that bindings can be simpler and more interesting in JRuby. The binding method can be observed invoking Java functionalities.
00:11:58.060 You can get a new binding by passing the current binding when initializing your Ruby program. New bindings can fork from existing ones, and when we create blocks, the current binding is provided, meaning there is no need to copy variables. They are on the heap when needed.
00:12:39.130 When we call each method, we create a new block, passing the current body.
00:13:30.600 Now, returning to MRI, what can we do with binding? We can retrieve local variables from it, and it allows us access to their values. It was quite interesting when Ruby 2.2 had a bug.
00:13:52.640 There was an occurrence where calling binding resulted in local names from a method that hadn't yet been called. This situation would lead to crashes in the Ruby interpreter, but fortunately, it was fixed quickly.
00:14:24.040 We need to exercise caution when invoking binding, especially when it comes to lambdas. A lambda keeps the variable in scope due to its binding, meaning any change made to that variable will also affect its scope.
00:14:55.520 As an example, if we create a local variable and two lambdas, one incrementing it and the other decrementing it, after calling both lambdas, the variable will be modified by their operations.
00:15:40.240 The final value of the variable will be the result of the lambda operations. This is because they share access to the same stack frame's copy of the variable.
00:16:00.000 One question that arises is can we change the binding we are working on? The answer is: almost. If we create a new lambda and set an instance variable there, that variable will not be available outside the lambda because the original stack frame does not contain it.
00:17:07.300 When we call binding from within an object, it highlights the binding's scope. This way, we can return outstanding content from the scope.
00:17:51.700 Moreover, we can evaluate within the binding's scope. For instance, by setting local variables, we can execute an addition operation on them. Everything will work beautifully.
00:18:22.080 Since binding is an object, it can access local variables, receivers, etc. This makes bindings a useful tool in debugging. This is essentially how Pry works.
00:19:00.140 When you call binding in Pry, you can access all local variables. However, using Pry with binding may limit access to local variables, only allowing access to instance variables.
00:19:40.970 Since binding provides access to local variables, it can also be used in templating. For example, if you have a template variable that is not defined, invoking it will generate a NameError.
00:20:24.040 However, by calling it inside a binding that contains local variables, the template can render correctly.
00:20:56.900 I also wanted to briefly highlight the Raise gem that works nicely as a debugging tool. By attaching bindings, we can create some micro containers for variables.
00:21:38.890 As a fun project, I worked on a version of OpenStruct that utilizes bindings. The first version is basic, only performing method missing to access local variables. Version two introduces caching, while version three deals with performance improvements significantly.
00:23:09.970 The code is available on GitHub for further exploration. Additionally, I created a gem named BB OpenStruct, which provides OpenStruct-like functionality using bindings.
00:24:02.470 As a final note, when inspecting LLVM bindings, we find that they operate almost the same as lambdas with one notable flag that determines whether it behaves as a proc or a lambda.
00:24:53.800 Thank you very much! Are there any questions?
00:25:06.210 Okay, not really? Thank you!
00:25:15.000 Thank you very much and goodbye.