I recently had the pleasure of chatting with Piotr Solnica about Ruby, Rails, dry-rb, functional programming, and the Ruby community.
What I find interesting about Piotr’s work is how it incorporates concepts that are somewhat-foreign to the typical Rails developer. Rails has dominated the Ruby ecosystem for a long time, but it is just one tiny part of the overall software development industry. Piotr, and developers like him, play the important role of breathing fresh air into our community.
Without further ado, I’m proud to present: Q&A with Piotr Solnica.
Tom: Let’s start with an overview of dry-rb. What is the general idea behind dry-rb? Why does it exist? Was it created in response to something missing in the Ruby ecosystem?
Piotr: The general idea behind dry-rb is to create a collection of reusable libraries that solve common problems. It was brought to life in a natural way – Andy Holland and I built a couple of libraries that we needed. Eventually we decided to put them under the same organization on GitHub, and continue working on them together. More people joined us quickly, and the organization started to grow. Initially we just built a couple of things that didn’t exist at all, or we were not happy with the existing solutions, and that’s how it still works.
We’re promoting a modern approach to Ruby, as we’re leveraging both OO and FP features. There are various things that are common in the Ruby ecosystem which we deliberately decided to avoid, like monkey-patching or relying too much on object mutability, including global mutable state in the form of classes or modules. dry-rb also promotes various design techniques that are less common – dependency injection (DI), object composition, and type safety.
Tom: With things like DI containers and FP concepts, it seems like the dry-rb style of code is a departure from what you could call “normal” Ruby/Rails code. What kind of reactions are you getting from the community, both positive and negative?
Piotr: Ruby developers hate the word “container,” ha ha. Seriously though, reactions are mixed, which is not surprising. We’re essentially changing the way you think about solving problems in Ruby, by gently pushing towards functional programming. This is something that’s commonly criticized because many people think Ruby is “just” an OO language, and what we’re doing is not natural, and there are better languages for this approach. I certainly agree that some patterns are simpler to achieve using languages that were designed from the ground up to support them, but it doesn’t change the fact they are easy enough in Ruby. On the other hand, techniques like DI are much simpler in Ruby than, let’s say Clojure, or a statically-typed language like Scala. I personally think there’s much more to Ruby than people commonly think, and I do not agree that Ruby is “just” an OO language, because it’s a multi-paradigm language that combines OO with FP. Even its own description states that.
Another negative reaction is based purely on preferences when it comes to specific DSLs or APIs. Some people simply don’t like, let’s say dry-struct, and they prefer Virtus. Others prefer one-line validation macros in ActiveModel, instead of explicit schema definitions with composable predicates in dry-validation. Others may not be interested in learning about control flow based on monads, as they simply prefer whatever they are doing now. We’re partially “guilty” of such reactions, simply because dry-rb is a young project and our documentation needs a lot of love, but we’ll get there. I’m sure with more documentation and various resources, more people will approach dry-rb with enthusiasm.
Luckily, I’m seeing a lot of positive reactions too, and the community is growing very fast. Something that makes me especially happy is when I hear that somebody achieved something easily that wasn’t possible before with other gems, and custom code would have been too complicated. We also get positive feedback from other communities where dry-rb gems were adopted – Hanami and Trailblazer. This has been the goal – to create reusable libraries that solve complex problems, so that they can be used in other gems and low-level abstractions.
Tom: You’ve been working with Rails for about 10 years now. I’m sure you’ve written and maintained a lot of typical Rails code. Yet, at some point, you made the decision to write your Ruby code in this different style. As someone who has experience with both styles of Ruby code, what are the pros and cons of the dry-rb style? What would you say to people who are comfortable with Rails, but not very experienced with FP, and are curious or hesitant about switching?
Piotr: Making my Ruby code more functional has made it simpler and faster. It’s been a long process to get there though, and it included years of experimentation and learning. Gems from dry-rb provide many abstractions that make it easier to write Ruby in the OO/FP style, and it results in application design that’s more robust and simpler to reason about. Because the gems are small, you have much more control over your code and you have a better understanding of how things work together. Crucial parts of your systems are handled explicitly, like data validation, type conversions, and error handling. These are the areas which are typically not handled with enough attention, and as a consequence our applications are buggy and potentially insecure.
Another advantage of this style of programming is composition. When you use callable objects that don’t rely on state mutations, it’s much easier to compose them. Furthermore, the objects tend to be very small and focused on a single concern. Because we made composition so ridiculously simple through constructor injection and automatic dependency resolution, adding new components to your application often boils down to composing a couple of existing objects and adding few small, new bits of code. This really changes everything. I’ve been trying to follow various good OO design principles for years and I was never satisfied with the results until I changed my approach to OO/FP. Two key factors that helped me improve the design were moving away from relying on mutable state, and isolating the complexity of object construction via IoC containers.
The downside of dry-rb gems is… the same thing that makes them great: they are small, simple abstractions, and because of that, people who are used to more complex, well-integrated frameworks will simply have a steep learning curve. This is partly because the documentation is not complete yet, and we’re missing user guides. This will be improved over time, but at the end of the day, dry-rb gems will remain simple abstractions, and it will always require more work to use them standalone vs using a full-stack framework. We should remember that it comes down to tradeoffs. In this case, you’re trading initial ease of use and fast prototyping for more control over your own code.
People who are comfortable with Rails but without much understanding of FP concepts would have to learn the basic principles of FP design. The biggest challenge is wrapping your head around the idea of objects that don’t change. Once you learn that there is really no need to change objects, things will become more clear. Another thing is the data-centric approach, where you think about your application as a series of data transformations rather than object interactions, where data is part of state in order to “encapsulate” it. This means treating data as a first-class citizen. It doesn’t mean your code is no longer OO. We still use objects. In fact, we use objects way more than in a typical Rails app. The big difference is that the data is first-class, and it’s passed around as values. Individual objects process these values without changing them, and simply return some results, typically new values.
A simple example: using Rails, in order to persist data you might:
- create an ActiveRecord instance with the received params as its attributes
Whereas in our world, you:
- validate the params, which may involve coercions
- ask a repository object to persist validated data
This may look similar, but in reality it’s a different design that makes things more obvious and manageable.
Tom: For people facing this steep learning curve, how can they get started? Can you recommend any books or presentations? Which dry-rb gems are the most approachable?
Piotr: I would recommend Functional Programming in Scala by Paul Chiusano and Rúnar Bjarnason. It’s not about Ruby, but it explains the fundamental concepts of functional programming. Just like Ruby, Scala is a multi-paradigm language, which makes this book a great intro to FP for an OO programmer. If you prefer something closer to Ruby syntax-wise, you could check out Programming Elixir by Dave Thomas, as it has a nice intro to FP programming and explains the basics of Elixir in a simple way. Syntax similarities between Elixir and Ruby may trick you a bit, so be aware of that.
When it comes to presentations, I don’t just recommend, I urge you to watch all the talks that you can find by Rich Hickey, the creator of Clojure. The especially important ones are:
- Simplicity Matters presented at RailsConf 2012
- Simple Made Easy from Strange Loop 2011
- The Value of Values
If you don’t feel like checking out resources from other programming communities, I would recommend watching Tim Riley’s talk Next Generation Ruby Web Apps with dry-rb, ROM, and Roda from RedDotRubyConf, which is specifically about dry-rb gems and other tools that we use, such as rom-rb and Roda.
Another talk that should be helpful is my Blending Functional and OO Programming in Ruby from BaRuCo 2015.
The most approachable gem in dry-rb is probably dry-struct, as it’s got similar syntax to Virtus, and it provides abstraction that many people are familiar with – typed struct objects. It should also be easy to get started with dry-validation for simple data validation, like request parameters, as a replacement for strong parameters in Rails. Once you feel more comfortable with dry-validation, you can start using it for more complex validations.
Tom: When topics like FP and DI come up on reddit, I sometimes see people claiming that it’s all theoretical, and doesn’t work in “real” codebases. I know that you might not be able to talk about specific details of your work if it’s proprietary, but can you give us the gist of your experience applying all this to real codebases? What does your day-to-day work look like?
Piotr: Yes, there’s a lot of prejudice towards DI in our community. After DI was “announced” as “bad” in Recovering From Enterprise by Jamis Buck, basically the whole community said “yep, DI is bad.” There are a lot of incorrect interpretations of this talk, and I think people should revisit this topic once again. To make it worse, DHH wrote Dependency injection is not a virtue and it made this anti-DI sentiment even stronger, as DHH has a gigantic influence in our community. This makes our job very hard, ha ha.
Applying DI to real codebases works very well. It works especially well if the whole application is done using DI, and if it’s not Rails.
I used “manual” DI for a long time without any gems, by using a pattern where construction logic was always implemented in
initializewas always a trivial method accepting abstract dependencies that it would assign to instance variables. This was an improvement, but class definitions tend to get hairy very quickly, as object construction logic can be quite complex. I never liked that, but I was still under the influence of “IoC is bad, containers are bad, it’s Ruby so keep it simple.” It took me a while to realize that I could use an IoC container with a very simple interface, and it would help me clean up my classes. Luckily, dry-container was born during that time, and I adopted it instantly.
After using containers for some time in real projects, I was still bothered by the amount of boilerplate code – defining attribute readers, defining initialize methods with identical logic, and so on. That’s how dry-auto_inject, my first dry-rb contribution, was born. It defines all this stuff for you, and you can configure it with your own container, which could simply be a
Hash. This removed a ton of boilerplate code and was a big step forward.
Initially I was applying these techniques to Rails apps, but I was never fully satisfied with it because of how Rails works, with all its auto-loading, globals everywhere, railties pulling in too much code in places I didn’t want it, and especially problematic hot code reloading. Despite that, I was very happy with DI/IoC via dry-rb gems in Rails apps, because it allowed me to achieve something I wanted to have since roughly 2011: a clear, application-specific API. For the first time, I could open a Rails console and use my application. Every action that my app could perform was easily accessible. I never had to tinker with object construction like, “Huh, lemme see how to instantiate this thing. Oh, it needs this config. Oops, it needs that object. Oh noez that ENV var must be set too, gaaah!” This is the kind of stuff that doesn’t happen with DI and containers – you just ask for a given object and there it is, go ahead and use it.
This experience was a true epiphany for me. Apart from that awesome side effect of being able to easily use my app in a console, the actual benefit is that it’s much easier to keep objects decoupled and draw boundaries between various components of your system. This allows you to add new functionality easily, as objects are more reusable. It’s also much simpler to replace one component with a different one, because we have duck-typing, and all we need is a compatible interface. I had cases where one object dependency started as something very simple, used by a couple of other objects, and then it grew into something much more powerful and robust, and it didn’t require changing code in other places. This is only possible when boundaries are well established, and DI helps here a lot.
Anyhow, because I wasn’t happy with Rails, I started experimenting with alternative ways of managing application state – managing
$LOAD_PATH, loading files, building objects, handling configuration, etc. – and that’s how dry-system was eventually created. We made a small gem for building web apps with it called dry-web-roda (more routing front-ends are coming soon!) and this is basically my personal perfect setup. It’s not a full-stack framework though, so it’s not for everyone :) I built a couple of production applications with it and was very happy with how it worked.
This doesn’t mean there are no challenges and problems. For example, fast booting is really needed in development, and we’re trying to avoid hot code reloading. For small to medium apps, it’s already very fast. Loading a medium-sized app using dry-system is as fast as loading a vanilla Rails skeleton. The view layer is in flux, and we’re trying to figure out how exactly we want it to work. Integration with rom-rb could be better, especially for multi-app setups, where you’d like to easily share persistence components across few apps.
My day-to-day work is Rails these days. I try to use some of the dry-rb gems wherever I think it will help. We’ve started using dry-validation and dry-types lately, as we’re dealing with a lot of data from 3rd party APIs, so processing and validating it properly is crucial.
In general, I can safely say that it’s just hard to introduce new concepts at work. It’s a reflection of the Ruby community. People have learned certain tools, patterns, and techniques, and they are productive when using them, so justifying a change of approach is challenging.
I’ve been in this business long enough to learn that you need to be pragmatic and responsible when building software for clients. Consulting work means that you’re gonna build something, and at some point you’ll hand it over, and there’s always a chance another developer or a whole new team will take over. This means you need to build the software in a way that will make it simple to develop for other people in the future.
There is, however, a question I ask myself very often. After seeing countless messy Rails codebases, which are hard to work with regardless of whether you’re a Rails pro or not, is it really always a good idea to stick to what’s common and well known?
There are cases where using an alternative approach is easily justifiable. Once I had to build a system which imports data from deeply nested, very complex data structures. I picked rom-rb and its mappers for this, and
ActiveModel::Validationextensions because it felt like a good fit. The approach was completely different from ActiveRecord, but it made my job simpler. Validating complex data structures was possible with rom-model, and converting them into something we can easily persist was possible via rom-mapper. The code was complex, but it would’ve been way more complex with ActiveRecord and “plain” ActiveModel, as they don’t support complex validations of nested data structures, and there’s no concept of mapping data at all. This was a great project, and I actually started working on dry-validation after a gigantic struggle with
ActiveModel::Validations, when I realized things could be done better. Various unique features of dry-validation have been implemented specifically because it wasn’t something easily achievable in
ActiveModel::Validations, and I badly needed them. Too bad I didn’t have a chance to refactor this project to use dry-validation – lots of code could be removed!
Tom: It sounds like the challenges are mainly social, not technical, like so many things in our field. I’d like to know what you think about the social landscape of Ruby and Rails right now. Do you feel that the Ruby developer community is generally healthy and growing? Have we become overly focused on Rails and Rails-style code? Are we too resistant to external ideas and practices? What direction do you think we are heading over the next five years?
Piotr: I wouldn’t call the Ruby community healthy. Growing? Yes, definitely, but the environment in which that growth is taking place is a difficult one.
I do think we’ve become overly focused on Rails. It used to be this amazing driving force, and made Ruby so popular, but it’s now slowing down progress.
In our industry, practically speaking, there’s no Ruby – there’s Rails. In job offers you don’t see companies looking for Ruby developers, they are looking for Rails developers. People may think this is great. It means that Rails is so fantastic that simply everybody is looking for Rails devs, but there’s another side of this story. People who want to use other solutions will have a really hard time finding a job where it’s possible to do so. I know companies where Rails is no longer the primary tool, but it’s a fraction of all companies where Ruby is used. And exactly this makes it very hard to create alternative solutions.
Let’s keep in mind that we’re talking about open-source software here. All these libraries and frameworks are mostly built by contributors whenever they find time. When companies keep insisting on using Rails, other solutions will not grow and become viable alternatives as fast as they could if more people decided to give them a chance.
Regarding resistance to external ideas, it’s natural and I completely understand that. When you take into consideration the business side of things, it’s no surprise that most companies keep using Rails. It’s just a safer bet. The question is, what are we going to do about it?
I see two options. One is that we just let it go. Ruby is going to remain that language used by Rails. Lots of people will move on to a different technology. The second option is that we take a really good look at what we’ve accomplished as a community over the last decade or so, and see if maybe we can use all that great experience to improve our ecosystem, which will help Ruby remain more relevant in the future. That’s pretty much what I’ve been trying to do over the last six years.
It’s hard to comment on the direction we’re heading. It’s just a big unknown. I see how Rails keeps influencing the ecosystem and even the language itself, and given that it creates a very specific programming culture, I’m worried that it’ll be very difficult to change that. Luckily, there’s a very strong community built around alternative solutions. I honestly had no idea things would speed up so much during the last year or so, but it doesn’t change the fact it’s incredibly hard work.
Whenever you try to promote alternative gems, you should be prepared for very negative reactions. I remember back in roughly 2007-2009 there was way more energy and enthusiasm. People were more open and excited about all the new things that were popping up almost every month. Now it’s different. We’re a mature community. We have our ways of doing things. People are much more interested in a new ActiveRecord feature than some new gem, as it has an immediate impact on their daily work, whereas checking out new gems is not really our thing anymore. I’m of course generalizing a lot now.
There are a lot of great, experienced Ruby devs who are working on and supporting alternative solutions, but it’s still a small percentage of the whole community. Given that there’s not much support from the industry and companies keep picking up Rails, it’s just hard to predict how things will evolve.
So, overall, it’s hard to say what direction the whole community is heading. It feels like community fragmentation has happened lately, and we may just see that there’s no longer a single Ruby community. Is that a problem? I don’t think so. We’re going to duplicate efforts to some extent, solving the same problems, but we’ll be doing it in a different way. All solutions have their pros and cons, and it’s just important to have options to choose from, that’s all.