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.