Alex Wheeler

Rewriting Rack: A Functional Approach

Rewriting Rack: A Functional Approach

by Alex Wheeler

In the video titled "Rewriting Rack: A Functional Approach," Alex Wheeler discusses the potential of functional programming (FP) principles to enhance Ruby's Rack framework. The talk emphasizes the transition from object-oriented programming to functional programming paradigms to create simpler and more maintainable code. Key points include:

  • History of Programming Languages: Wheeler illustrates the evolution of programming languages, starting from Fortran to modern languages like Ruby and Clojure, highlighting their contributions to current programming methodologies.
  • Concepts of Functional Programming: The speaker articulates the core aspects of functional programming, such as immutability, simplicity, and the first-class status of functions, which allow greater abstraction and less error-prone code.
  • Rack Architecture: An explanation of Rack as an interface facilitating communication between web servers and Ruby web applications, emphasizing its flexibility and simplicity.
  • Procs and Middleware: Wheeler demonstrates using procs as an alternative to classes for building Rack applications, showcasing their capabilities in defining middleware easily. He illustrates how this approach can lead to clearer and more concise application structures.
  • Case Study on State Management: He discusses challenges with mutable state in traditional object-oriented programming and how functional programming principles can effectively mitigate issues such as unintended data mutations and complications in multi-threading environments.
  • Encouragement to Explore: The talk concludes with encouragement for developers to experiment beyond their comfort zones, suggesting that integrating functional programming principles could streamline development practices and foster clearer programming structures.

Overall, the talk advocates for leveraging functional programming concepts within Ruby to simplify coding practices, reduce bugs, and improve code readability, all while maintaining the playful nature of Ruby programming.

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!