00:00:11.250
Well, hello everyone! Welcome to my talk, "Rewriting Rack: A Functional Approach."
00:00:17.110
Thank you so much for coming! I'm super stoked to be here, and I’m glad you all made it.
00:00:24.580
I'm Alex Wheeler, and feel free to tweet me, or ask me any questions, comments, or concerns.
00:00:30.610
You can find me on Twitter and see what I'm working on at Alex Wheeler on GitHub.
00:00:36.160
I currently work at VTS, where we develop commercial real estate asset management software.
00:00:41.530
We manage just over six and a half billion square feet on the platform, and it’s all powered by Ruby.
00:00:47.260
But I started thinking, if we had built VTS back in 1956, we definitely wouldn't have used Ruby—we might have used something like Fortran, which is represented here by a punch card.
00:01:00.280
What you would do is punch in an instruction set, hand it off to a huge machine like the IBM 704, and a few hours, days, or even years later, you might receive a result, assuming you had no bugs.
00:01:09.399
This was considered high technology at the time. A few years later, John McCarthy, known as the father of Lisp, approached programming differently.
00:01:16.990
While Fortran worked well for large organizations with straightforward problems, McCarthy was focused on artificial intelligence. He needed a much more interactive computing environment, so he built Lisp to allow for questions and instant feedback.
00:01:38.170
With Lisp, we got the first REPL (Read-Eval-Print Loop). Just like when we open up an IRB session or a Rails console today, we get that instant feedback that traces back to Lisp—a technology developed in the 1950s.
00:02:04.000
A few years later, we have Grace Hopper, who led the team that built the COBOL programming language. If you've never seen COBOL, here’s a simple function: it reads just like English. This was groundbreaking, as she wanted to build software using languages that felt natural and read easily.
00:02:27.340
Fast forward to 1993, when the public web emerged. This was also when Matz started working on Ruby. A few years later, in 1995, we saw the first public release of Ruby. Fast forward again to 2004, when Ruby on Rails came into existence. This is likely when many of us first began writing Ruby, building web applications using the Rails framework.
00:02:57.790
And here we are today, in 2017, with you all here to listen to me speak for 40 minutes, which is super cool. But the future is now, and while we like to think we've mastered software development, the truth is, we still face challenges. None of our programs are perfect, and we're still working within a young industry.
00:03:30.670
What we do have, though, is Ruby, and the genius behind it is that it wasn’t created in a vacuum. Matz looked to past technologies, including those we’ve discussed.
00:03:43.120
Some of those influences include Smalltalk, Lisp, Perl, and even Python. If you've experienced exceptions in your code, you've probably seen Python exceptions, which we can thank Python for. Additionally, we have cryptic global variables in Ruby, which we’ve borrowed from Perl.
00:04:20.340
If you plan to use these variables, make sure to require 'English' for better references. Thus, rather than being seen as cryptic globals, they become simply globals.
00:04:54.590
In Ruby, we also have blocks. You’ve likely iterated over collections using methods like map or each, passing these blocks to yield each value. This concept can be traced back to Lisp from the 1950s. Ruby also has real closures, allowing a block of code to be bound to certain local variables that can be executed in different contexts.
00:05:26.570
What’s fascinating is that Ruby does not have true primitive types; everything is an object. As previously mentioned, in Ruby, there are no strict rules. We could redefine the plus method in the integer class. In our new world, three plus one could equal two, which can be either exciting or scary.
00:05:49.130
This flexibility comes from concepts originally found in Smalltalk, where everything is an object. Matz blended those ideas into Ruby, creating something extremely engaging.
00:06:23.300
So, what do all these languages and concepts have in common? They aim to simplify programming. We don't build new languages or technologies just for fun; many are designed to make our lives easier.
00:06:49.550
Matz spoke about pushing work onto the machine and having humans do less because programming is tough. You've probably heard the term "plus or minus seven"—the number of things we can keep in mind at once.
00:07:10.580
With that in mind, let’s consider the many ways we can call a proc. We can pass it one argument, which gives us the exact result. There’s some syntactic sugar: you can call procs using the call message with the argument one, and you can enforce stricter rules with ‘public send’. Procs won’t enforce the arity of the closure, allowing any number of arguments to produce the same result.
00:07:27.080
This concept relates to functional programming, which centers around organizing code with functions instead of objects, while emphasizing immutable data. These ideas might seem advanced, but the overall objective is straightforward: to simplify our code. We've been striving to make programming easier since the 1950s.
00:08:04.760
When I began to hear about functional programming from friends and colleagues, it sounded like an advanced concept for projects like distributed blockchains that I wouldn’t use in my everyday work. This curiosity led me to explore Clojure, a functional language and a Lisp variant that runs on the JVM.
00:08:44.230
As I implemented my Ruby concepts in Clojure, things clicked. I realized that adopting functional programming allowed me to sidestep a wide range of problems present in more conventional programming styles. Let's dive into some basic concepts from Clojure.
00:09:14.949
In a basic Clojure file, we work with vectors, akin to Ruby arrays, and maps, which are similar to Ruby hashes. When we call a function in Clojure, we wrap it in parentheses. For example, asking for the value associated with 'name' returns 'Alex' when we run it.
00:09:43.540
We can also perform basic math with nested function calls. Defining a function called ‘full name’ allows us to concatenate first and last names easily. In Clojure, functions are first-class citizens, much like in Ruby, which gives us more flexibility.
00:10:25.000
Now, let’s imagine we have a variable ‘comp’ in Clojure, binding it to a map. If we change 'name' from 'RubyConf' to 'ClojureCon,' we receive a new map, but when we check the identity of 'comp', it remains the original. This illustrates the immutability principle in functional programming where variables cannot change once assigned.
00:11:02.700
This principle aligns perfectly with Clojure and functional programming’s ideology. It promotes the abstraction of functions, allowing us to navigate our programming needs without muddling with mutable state.
00:11:53.110
I love this quote by Alan Kay: "A change of perspective is worth 80 IQ points." By the end of this talk, my hope is for us to explore Rack through the lens of a functional programming approach.
00:12:10.839
Even if you're unfamiliar with Rack, let me explain: Rack is straightforward. On the left, we have a web browser like Google Chrome, and on the right, a basic Ruby web application. In between, we have Unicorn, which acts as a web server.
00:12:41.180
Unicorn’s role is simple: it waits for requests to come in over the wire and hands them off to the web application, which processes them and returns the response back through the wire. This architecture isn’t limited to Unicorn; there are several other web servers, such as Puma and Webrick.
00:13:06.590
What would be fantastic is if today we're using Rails but, tomorrow, we could switch to Sinatra without changing our code that interacts with the web server. Rack enables this flexibility by defining a simple interface with a few basic rules.
00:13:52.470
To be Rack-compliant, a Ruby web application must define a method called 'call' that takes the request as an argument. The Rack-compliant web server calls that method, passing the request as a Ruby hash. The response must return a triple: an HTTP status code, a hash with headers, and the response body. As long as this structure is followed, we can easily swap web applications.
00:14:49.710
Let’s consider the simplest Rack application we can build. We have a class called App (though we could name it whatever we want)—we define a call method that takes a request and returns the required triple. To run this app, we require Rack, instantiate our app, and run it with a web server like Unicorn. If we wanted to switch to Puma, we just have to change out the adapter.
00:15:47.370
Using just three lines of code, we’ve built a Ruby web app, which is pretty neat! But we can actually take this a step further; since procs respond to 'call' too, we could build a Ruby web application in just one line of code.
00:16:04.340
Instead of the class-based approach, we can leverage Ruby’s lambda capabilities. Thus, we can create our app in one succinct line. Rack also provides a DSL for crafting these applications.
00:16:31.940
We can initialize a new builder, define our app, and execute it. You may wonder why this approach differs from using a proc. The difference lies in middleware, which allows us to enhance our web application functionality without modifying its core.
00:17:16.780
Feeling curious? The middleware can wrap the request, allowing various operations between the server and the application—like logging actions, caching responses, etc. Let’s examine how middleware is structured: it needs to define a call method, in addition to knowing about the next middleware in the stack.
00:17:54.480
When a request comes in, it calls the middleware’s definition. We can scratch the surface of how Rack Builder operates. While the code is extensive, the essential function is to create a stack of middleware through which a request will pass until it reaches the final application.
00:18:53.370
For example, if we were to use both logger and cache middleware, they would be initialized in a stack until we reach the Rack application. Once the request flows through each layer, it concludes with responding to the client.
00:19:30.780
Now, let’s consider this: can we practice functional programming in Ruby? I believe we can! If we use functions over classes, we can avoid traditional object-oriented structures.
00:20:28.140
In Ruby, we don’t have first-class functions, but we do have procs as bindings in contexts. For instance, we can implement middleware simply by using procs in place of classes but retaining similar functionality.
00:21:25.510
Continuing from our middleware implementation, we can implement it effortlessly without relying on class structures. This approach keeps our code tidy and maintains readability.
00:21:56.590
We’ve seen how we can execute the same logic through this alternate approach. It may seem trivial; however, there are significant advantages to simplifying code.
00:22:23.730
The flexibility in Ruby allows us to strip down constructs, rearranging them syntactically without losing functionality. We can express similar concepts found in languages such as Clojure while maintaining Ruby's playful syntax.
00:22:51.130
Let’s reexamine our middleware constructs for more clarity. What we produced here mirrors structure found in Clojure, reinforcing our notion of deriving from functional programming while working in Ruby.
00:23:19.650
While I may seem unconventional, my objective is clear: we can streamline Ruby's approach by exploring functional programming concepts. Eliminating reliance on large libraries just raises questions. Let’s challenge how we perceive programming structures.
00:23:55.030
As developers, we often look towards writing more code, but we should consider whether we need to complicate that process. What if we aspire to minimize code instead of adding to it?
00:24:21.420
Many bugs arise from shared mutable states, often leaving us frustrated and questioning our choices. These bugs can detract from performance and lead to costly errors in practices.
00:24:40.170
Let’s visualize this through a case study using Rack middleware meant for court verdicts. The situation arises when the request updates with a verdict message. Adjusting to include names instead of verdict types adds excessive complexity.
00:25:17.020
This cycle leads to clunky designs, where maintaining state becomes a struggle. To remedy this, we typically craft convoluted functions and variables that become challenging to trace.
00:25:54.170
As we scale, opting for multi-threaded servers like Puma introduces unexpected behaviors. The absence of strict variable binding in languages, like Ruby, can cause catastrophic errors in logical constructs.
00:26:29.490
Through investigative steps, we find ourselves in setups where mutable structures end up causing data retention that cross-contaminates between requests, leaving us with undesirable results.
00:26:55.950
Subsequent debugging cycles often reveal the underlying object IDs linger across requests. In functional languages or Clojure-style environments, this risk becomes mitigated through their inherent designs.
00:27:22.080
We need to ensure that techniques in Ruby do not create imbalances. Consideration needs to be given to keeping instances reentrant or immutable to avoid unwanted side effects.
00:28:06.780
Moving onward, as we engage these layers, a glaring flaw may reveal itself in the way data is processed. The recursive layers we've come to rely upon can seem functional, yet devoid of the previous insights.
00:28:44.990
In lessons derived from our middleware concepts, when requests come in, we must ensure data remains consistent across usage boundaries.
00:29:10.920
When we retrieve variables, we cannot simply update hashes without understanding their behaviors. In Ruby, we might find ourselves caught switching contexts unexpectedly, leading to mishaps that threaten data integrity.
00:30:22.640
These unforeseen events can lead to large issues within applications, causing us to question the viability of our programming methods.
00:30:38.250
Proposed solutions often revolve around cloning instances, which act similarly to functional programming principles by creating new contexts, though this adds significant overhead.
00:31:05.490
There's reliance on methods such as 'freeze' to ensure immutability in our processes, and effective use of such constructs can prevent critical cross-pollination.
00:31:35.420
But remember this: if we’re just keeping track of where state resides, we might find methods inhibited as undesirable overhead.
00:31:59.170
Understanding object ownership in a mutable state can ultimately aggravate our approaches to functional contexts. These challenges of classic OOP lend themselves to wider structural problems.
00:32:21.470
The systems often lead us astray into development storms of responsibility and handedness in data. Abandoning conventions for maps and hashes lends us the opportunity to explore the roots of our program without unnerving encounters.
00:32:43.470
Ultimately, the integration of functional principles into our Ruby repertoire paves the way for shared understanding, preventing disjointed showcases of our endeavors.
00:33:14.670
By merging these languages, we evolve our practices. To conclude, it’s crucial to explore beyond our comfort zones.
00:33:36.910
The journeys into other communities can result in groundbreaking discoveries. Lastly—whatever challenges we face—enjoy the process of exploring programming!
00:34:04.620
Thank you for your time and attention!