Ruby
Harness the power of functions to build composable rack applications
Summarized using AI

Harness the power of functions to build composable rack applications

by Marc Busqué

In this RubyConf 2021 talk titled "Harness the Power of Functions to Build Composable Rack Applications," Marc Busqué explores the concept of functional programming through the lens of building web applications using the Rack framework and a new abstraction called Web Pipe. The key points of the discussion include:

  • Understanding Functions: The talk begins with an explanation of functions as black boxes, which take inputs and return outputs, referencing both mathematical and programming contexts. The concept of pure functions is introduced, along with challenges such as hidden preconditions and side effects in real-world applications.

  • Rack Framework: The presentation transitions into the Rack framework, explaining how Rack applications respond to HTTP calls. The distinction is made between modifying requests before they reach the application and altering responses afterward, illustrating the middleware concept.

  • Introduction to Web Pipe: Web Pipe is introduced as a thin layer on top of Rack, allowing the construction of web applications using function composition. Here, each function operates as a complete Rack application, leading to a more streamlined and efficient design.

  • Key Features of Web Pipe: Busqué discusses how Web Pipe's structure allows for the creation of modular applications by defining functions that take and return the same data type. The importance of immutability and extensibility is emphasized, allowing developers to easily create new features or adapt existing ones.

  • Integration and Application: The talk exemplifies how Web Pipe can be integrated within frameworks like Hanami, showcasing its capability to manage complex application architecture while remaining flexible. It highlights the use of dependency injection, session management, and middleware integration within the larger context of a web application.

  • Real-world Usage: Several examples demonstrate the practical application of Web Pipe, such as configuring sessions, handling user inputs, and performing actions based on specific user authentication logic. The presentation concludes with an overview of how Web Pipe fits within various frameworks and its potential for improving Ruby application architecture.

Overall, the talk emphasizes the power of functional programming principles in enhancing the composition and modularity of web applications built with Ruby and Rack. The practical implications of adopting these principles in application design are highlighted as significant advancements for Ruby developers.

00:00:11.200 Hi everyone, and welcome to this talk.
00:00:14.400 First of all, let me thank you all for watching this. I also want to thank RubyConf for the fantastic organization.
00:00:24.160 Harness the power of functions to build composable Rack applications. We're here today to talk about web_pipe, a thin abstraction on top of Rack. As the title says, it allows us to build applications that can compose.
00:00:30.480 We'll see in a moment what this means. Besides presenting web_pipe, today's topic is aimed at those interested in functional programming. This talk should also help to distill the different actors playing together in a complete stack. We'll see how to build applications with a better architecture.
00:01:10.720 To begin with, we'll start with some basics about functions. Next, we'll move into Rack and its design model. Then we'll jump into web_pipe and walk through its main features. Lastly, we'll see it integrated into the context of the Hanami framework.
00:01:33.920 Before diving into the content, let me briefly introduce myself. I'm Marc Busqué, and these two are my sons, the meaning of my life. I'm a software engineer working mainly with Ruby since 2013.
00:01:40.880 Currently, I work at Nebula, the primary maintainer of the Salidus e-commerce platform. By the way, we are hiring, and believe me, it's one of the places where you'll find a better work culture. I'm also a member of TriRuby and Hanami, and I try to contribute my two cents to move them forward.
00:02:06.880 Alright, let's begin by asking a question: What is a function? No secret, a function is an opaque box that takes an input, does something with it under the hood, and gives back an output.
00:02:30.560 In more mathematical terms, a function is a binary relationship between two sets, where each element of the input set is matched precisely to one element of the output set. For example, we can talk about the function 'length'. This function maps every possible string in the world to an element of the set of integers.
00:03:01.200 Of course, when programming, things get more complex. This example is an instance of a pure function. We can provide the same input 1,000 times, and we'll always receive the same output and nothing more.
00:03:12.239 However, in computing, we always end up having to deal with two uncomfortable guests: hidden preconditions and side effects. Let's say we want to know the stock for a product in a given store. The same execution could return different values over time as the stock changes. On top of that, it might modify a counter for the number of visits.
00:03:52.480 In one of the most basic and common approaches, we capture these hidden preconditions and side effects in the snapshot of the current database state. Still, using functions as a fundamental abstraction when programming is beneficial.
00:04:08.239 To complete the picture, we need to talk about the star feature of functions: composition.
00:04:10.239 Two functions compose when the output of the first belongs to the same set as the input of the second. In that case, we can squeeze both functions and treat them as a single one. For instance, the 'length' function we saw earlier goes from string to integer.
00:04:51.759 Then, another function, 'greater than 4', goes from integer to boolean. We can compose both into a function 'longer than 4', allowing us to go straight from string to boolean.
00:05:06.960 In a way, programming can be seen as composing functions. We move from an input to an output using composed functions, which give us methods, modules, and objects.
00:05:51.759 There's an interesting type of function composition, where functions have the same type for the input and the output. These functions can compose an infinite number of times with themselves. For instance, the function that adds 1 to an integer can be applied recursively an infinite number of times.
00:06:06.960 Let's now move on to Rack and see if what we have discussed makes any sense in this context. As you probably know, a Rack application is anything responding to a 'call' method instance. This is a simple 'Hello World' application on Rack using a lambda function.
00:06:40.320 Like any other Rack application, it takes as an argument the request environment and responds with a triplet of response status, headers, and body. This means that if we want to decorate a Rack application, we have two possibilities: modifying the request before it reaches the application or modifying the response once it leaves.
00:07:14.240 This is another Rack application that decorates the 'Hello World' example we just showed. It adds something into the request environment and consumes it later to build the response.
00:07:51.760 We can abstract the decorated application; now it's an argument of the second application. However, as we know, to be considered valid by Rack, it needs to take a single argument.
00:08:21.760 We can fix that by partially applying the function, thanks to Ruby's currying facilities. The resulting lambda adheres to Rack's constraints.
00:08:53.439 This is just another way to replicate the same behavior usually done by so-called Rack middlewares. Middlewares are merely a convention; the decorated application is stored as a state of an instance, which itself is the final Rack application.
00:09:05.240 Notice how we store the application on initialization and then define the 'call' method. This convention is so common that it has a built-in DSL in Rack.
00:09:24.080 We use a given middleware, and we run the decorated application. If you are a bit lost, don’t worry, you can pause the video and think about it. Anyway, it's not going to be essential for understanding the rest of this talk.
00:10:11.360 However, I wanted to show this different way to build a Rack application because I want to highlight how it implements a two-way pipe model beneath the surface.
00:10:29.679 Take a look at this example. Here we have abstracted the text that we add to the response. First, we use a middleware to decorate it with one, and then we use another one to decorate it with two.
00:10:58.720 However, look at the response: the first middleware with options to modify the request is the last one capable of changing the response.
00:11:11.360 This slide better illustrates the two-way pipe that Rack implements. That's close to function composition, but we are not there yet. A Rack application's output is not the same type as its input, so they can't be composed. The best we can do is call one application from within another.
00:11:58.560 And that brings us to the main topic of this talk: web_pipe.
00:12:00.960 Web_pipe is a thin layer on top of Rack. It transforms that two-way pipe into a one-way pipe, constructed through function composition.
00:12:18.080 Look at the picture: we no longer have a distinction between the middlewares and the application. The only thing we have are functions. Each function is itself a complete Rack application.
00:12:58.880 The HTTP client is sending both the request and the response. The first function will receive an empty response. However, it's going to be built through the pipe until it reaches the client again. You write a web_pipe application by plugging functions that take and return the same data type. The type is an immutable struct containing the request information and methods to build the response.
00:13:48.000 As we saw, you can compose an infinite number of these kinds of functions. That's a 'Hello World' application written with web_pipe. We plug two functions together: the first one takes the connection struct and calls a 'not respond header' method on it.
00:14:02.240 That returns a fresh new instance of this struct with the content type set. Then, this new struct is handed over to the render step, which creates the response body.
00:14:34.880 If you're familiar with Elixir, you'll recognize this model as the same used by the Plug library. It was an inspiration for web_pipe.
00:15:00.480 In the last example, we defined the functions as blocks; however, they can also be provided as methods in the same class. Another option is to use a dependency injection container to resolve the operations.
00:15:50.080 We only need to specify the registration key on the steps' definitions. Here we are using Dry Container. If you don't know it, it belongs to the family of libraries within the Dry RB organization.
00:16:06.080 Web_pipe is designed to fit smoothly within the same ecosystem, as we will see shortly.
00:16:37.440 Modularity is another core concept in web_pipe. By default, web_pipe features are bare bones.
00:16:58.120 The connection struct only has the attributes to retrieve what we could think of as the primitive of a web request and the methods to build the web response.
00:17:25.440 There is also an 'add' method to include anything to the struct and then consume it later with 'fetch' downstream in the pipeline.
00:17:55.680 We also have a collection of extensions that add more everyday features. Furthermore, you can easily create new extensions.
00:18:30.080 Let's take a look at one of them. Parameters are just an abstraction on top of a URL query string or a request body.
00:18:50.720 To use them in web_pipe, you need to load them first. After enabling the extension, a new 'params' method is made available to the struct.
00:19:11.760 In this example, we greet the user by the username provided as a request parameter. We use the 'params' method to fetch the name and add it to the struct, and then we consume it later in the render step.
00:19:52.480 Notice how there is another change here; we have removed the 'content type' method and instead we are using a built-in plug. Web_pipe comes with a few of these for everyday operations.
00:20:12.960 We can make the parameters more compliant with traditional Ruby conventions by having keys as symbols. For that, we need to use a built-in config plug that allows tweaking how some methods on the connection struct work.
00:20:37.680 In this case, we configure the params extension to symbolize the keys under the hood. This extension uses Dry Transformer, another Dry RB library that helps streamline function composition.
00:21:12.560 Here we are applying some functions to the parameter hash. We are essentially in a kind of fractal-like function composition situation, which is pretty cool.
00:21:48.320 Now, let's stop for a second. There are some aspects here that in real-life development we might want to extract.
00:21:56.320 For instance, we don't want to set the content type or configure parameters at every action of our application. But think about it – we are building web_pipe applications by plugging together functions that take and return the same type.
00:22:40.640 If we look at the application as a whole from the outside, it is just an opaque box that takes and returns a connection struct. Zooming out from the fractal, we can extract the common bits into a standalone application and then plug it as if it were another step.
00:23:38.560 So finally, we made the title of this talk: we are composing Rack applications, but we are not done yet.
00:24:02.239 Let's go back to the design pattern behind web_pipe. Besides pure function composition, there's an extra feature that we need to consider: the ability to short circuit the chain and return early from an intermediate step.
00:24:50.239 Take again the example of saying hello to a given user, but now we will only authorize the greeting for some users. Our system only knows about two users: Alice and Joe.
00:25:32.480 We add a new authorized step after fetching the name. If the name is recognized by the system, we continue; otherwise, we return an error and halt the connection.
00:25:51.040 In this case, when the unfortunate condition is met, no further downstream operation will run; thus, the render step is not reached.
00:26:17.439 If you're familiar with managing that closely resembles composition, you might also know the term 'railway programming'. But if those terms don't ring a bell, it is still important to understand the concept.
00:27:06.720 Let's move forward to other web_pipe features. We can include middlewares in web_pipe applications.
00:27:54.080 To do that, we use the 'use' method, which adds the specified middleware to the generated Rack application.
00:28:17.120 In this way, we can take advantage of the large ecosystem of Ruby middlewares. In this example, we are combining the Rack session middleware with web_pipe's session extension. When the name is given in the parameters, we add it into the session.
00:28:57.920 Fortunately, we don't need to provide the name again for the second call to the action, as the name is already stored in the session.
00:29:20.960 Similar to what we did with plugs, we can bring the middleware from the base class, or we can use a shortcut method composed to utilize and plug at once.
00:29:43.840 Thanks to functional programming, we've managed to develop a simple, powerful, and beautiful API. However, one of the beauties of Ruby is its flexibility, allowing us to blend functional and object-oriented paradigms.
00:30:26.080 Due to this latter capability, web_pipe comes with options to easily inject plugs and middlewares.
00:30:56.800 Here is how it works with plugs. We only need to match the plug name with a new operation when initializing the web_pipe class.
00:31:22.560 Notice how we substitute the render step with a customized implementation. Injection is beneficial for testing purposes as it enables us to replace heavier operations and speed up the test suite.
00:31:41.440 So much for how web_pipe is perceived in isolation. But can it truly be useful?
00:32:21.520 An actual web application comprises a set of responsibilities.
00:32:36.960 How does web_pipe fit within the context of a web framework? Take Rails, for example.
00:33:03.280 As we all know, Rails implements an MVC pattern. You can see web_pipe as the 'C' in the controller. Rails router can dispatch requests to a Rack application, so there’s nothing stopping you from using web_pipe within it.
00:33:27.840 It even comes with an extension should you need further integration, such as rendering with Action View.
00:33:53.440 You can check the documentation for that. However, where web_pipe shines the most is in the context of a highly decoupled system.
00:34:17.600 And that brings us to the last section of this talk: Hanami 2.
00:34:45.120 Hanami 2 is a wholesale but assemblable application framework. At its core, it's a matrix made of three big components: Hanami libraries and the ROM database toolkit.
00:35:20.480 It's been developed by great people like Tim Riley, Piotr Solnica, Luka Guidi, and Nikita Shilnikov.
00:35:45.760 Each part of Hanami 2 helps create the whole but is replaceable with any other preference.
00:36:10.080 Unfortunately, we don't have the time to cover every detail that Hanami 2 provides.
00:36:29.600 Still, I want to show some snippets to convey how different responsibilities combine to build a full-stack application.
00:37:02.160 The following slides belong to a basic to-do application you can find on my GitHub profile.
00:37:29.200 Here you can see the application route definition. The 'slice' keyword creates a namespace and expects a sub-application within a subdirectory.
00:37:48.000 With slices, Hanami helps to decouple user code. You can still have all the code within a single repository, deploy it all at once, but you also have a structured organization.
00:38:14.080 As for the individual route definitions, the syntax should be familiar. The endpoints are keys provided to a dependency injection container that resolves to the web_pipe application that will be called.
00:38:50.080 For instance, let's consider the update of a to-do item.
00:39:08.640 Web_pipe is responsible for handling the request-response cycle, so it is the next actor in the chain. Initially, it composes the main application within the slice.
00:39:41.920 Let's delve into that for a moment. This application configures the session through the Rack session middleware. It also includes CSRF protection and a handy flash mechanism. It sets the content type, and lastly, it configures some data to be available in every template thanks to the Hanami view extension.
00:40:13.760 Pay attention to the 'fetch entity by ID' – this constant is defined at the top of the file. It's a higher-order function that returns a step trying to find an entity from a given repository by ID and helps when it isn't found.
00:40:59.840 Returning to the update application, looking at the plug definitions, it's clear what it does. It fetches a to-do instance, performs the required business transaction with it, and then renders a response.
00:41:21.680 We have just seen how we delegate the task of retrieving the entity to the repository. Now, let’s glance quickly at the transaction.
00:41:55.920 The transaction uses Dry Monads to bind together the individual operations that compose it. We only need two operations: data validation and persistence.
00:42:26.720 Regarding data validation, it delegates to a Dry Validation schema, defining a contract that the input data must adhere to.
00:43:00.720 Second, it calls a repository to perform the actual update. This is a RAM repository, which for this simple case requires hardly any code.
00:43:40.800 Now back to the transaction step. After calling the transaction, it pattern matches on the result. If it is a success, it continues; however, in case of failure, it binds the returned errors, delegates them to the view, and halts the web_pipe.
00:44:32.320 Conversely, if we follow the happy path, we reach the render step. Here, it simply sets a success message and redirects to the root path.
00:45:12.640 That was a quick glance at Hanami 2. Hopefully, you had the opportunity to see the impressive improvements it brings to Ruby in terms of architecture.
00:45:37.360 Please check the repo if you'd like to study it more closely. As for our talk, we are already at the end; I just want to thank you again for watching.
00:46:07.760 You'll find the slides and examples we saw in this repo.
00:46:16.799 If you have any questions, please use the Discord chat, open a discussion on the web_pipe repo, or reach me personally.
00:46:44.080 I would be delighted to help. Bye-bye.
Explore all talks recorded at RubyConf 2021
+92