Quick intro
I write Ruby code for living since 2007/2008, so you may call me a veteran (or a combatant if it pleases you). Along the way, I wrote some Java, lots of CoffeeScript and JavaScript, some bits of Python and even PHP, small amounts of Clojure and Elm. I have noticed, however, that none of these languages influenced the way I write Ruby code much - until I started writing Elixir for living too.
At the moment I split my work among two projects, one where I write Ruby, another one where I write Elixir. My Ruby style changed, and not on a concious level at first. This only became clear to me when I started looking at my old Ruby code and didn’t recognize it as something I’d write now days.
Avoiding chainable methods
The obvious influence is the use of functional programming paradigms in Ruby. Contrary to what you may think, I do not mean the chainable methods (as in Enumerable or own classes). In fact - if anything related to that - I learned to avoid chaining methods together, and go with simpler, better alternatives instead.
Chaining methods may have some advantages, especially for readability of the code in DSL-style applications, but it can also lead to problems with refactoring, bloating size of classes/objects and data and more difficult debugging.
Elixir provides the pipe operator (|>
), where you can chain function calls by
passing return value of previous function as first argument of next one
in chain.
There are some implementations in Ruby, but I didn’t find them that appealing. Instead, I simply use temporary local variable to perform method chaining in a dead simple manner:
def process_input(input)
tmp_value = step1(input)
tmp_value = step2(tmp_value)
tmp_value = step3(tmp_value)
step4(tmp_value)
end
This is not beautiful example of what you may call “style” but it is simple, readable, and does the job without side effects.
Encapsulating logic in small methods
I noticed, that I also write shorter methods than before. For example, whenever I
have if
-clause in my Ruby code, I would extract it to
maybe_do_something_now(data)
method, instead of keeping it inline.
This has unwanted effect on the size of the classes, but improves readability greatly.
This also helps in chaining function calls, to create data processing pipelines. If you have e-commerce platform, you could write code like this:
def order_total()
total = cart_summary()
total = maybe_add_shipping_cost(total)
total = maybe_apply_discount(total)
total = maybe_subtract_credit(total)
total
end
Limiting inheritance
We’ve been told to re-use the behavior of our classes and objects
through inheritance. This is a death trap if you overuse the mechanism.
Take a look at infamous ActiveRecord::Base
, that tries to do everything and as
a result is often unpredictable and leads to performance issues of your
application if you abuse it.
In Elixir you can’t really easily copy over functions from one module to
another. This is exactly what inheritance and including modules do in
Ruby, however. You have import
keyword, but it only aliases the remote
function that it can be referenced as local one, while still being
external (i.e. does not have access to module attributes of importing
module etc.).
I found myself extracting common behavior to external modules or classes
in Ruby, but I don’t include or inherit from these that much. Instead, I
do not mind creating utility modules and calling functions on them
directly. If I shared step3
function between my classes, I’d often
simply write:
def process_input(input)
tmp_value = step1(input)
tmp_value = step2(tmp_value)
tmp_value = UtilityModule.step3(tmp_value)
step4(value)
end
This results in smaller classes and objects, also provides cleaner encapsulation of functionality. We don’t have to create ActiveRecord::Base with all the functions we need anymore!
Limiting mutability
In Elixir, all data is immutable. Whenever you want to provide mutable
data, you need to use something like GenServer
or Agent
to keep the
mutable state.
In most procedural and object oriented programming languages, it’s the opposite: your data is mutable by default. And Ruby is no exception.
While Ruby allows you to freeze
objects (and later unfreeze ;)), the
simple pattern I use is to:
- Create new objects that take their initial arguments in constructor
- Process the arguments in constructor and set to object attribute
- Never ever again modify those object attributes. Instead, when we need to modify it, we do it in methods of object, or return new object with modified data.
An example could be:
class Counter
attr_reader :value
def initialize(payload)
@value = payload.to_i
end
def increment
Counter.new(@value + 1)
end
end
So instead of modifying the same counter, we always return new one if we need to.
But I found that in many, many cases, you never need to return a modified object. Take API calls response as an example. You get a response body, process it initially in constructor (de-serialize JSON?), set in an object attribute, and all you need to do later is to provide set of getters, that would modify the data internally in local variables
- but never modify object attributes again.
Writing concurrent code
Ruby programmers often shy away from writing concurrent code. There are good reasons for doing so, including performance penalties and decreased code readability.
Elixir inherits the concurrency model from Erlang, and you write a lot of concurrent code. Every time you want to have mutable state, you write concurrent code. Whenever you want to do something asynchronously, you write concurrent code.
In Ruby we would often reach out for background jobs for similar tasks. Quite often, prematurely and unnecessarily.
Yesterday I had to write API client, with rate limiting and internal caching. I could write some sort of locking, requests counting, many would probably even reach out to database to provide means of synchronization and lock between web worker threads.
I ended up writing GenServer
-like actor, spawned in separate thread,
that caches API response in internal state, and makes sure we don’t
exceed API limits. The whole thing is short, readable and works well for
now.
We have modern concurrency tools in Ruby now, we should not avoid using them. Celluloid is an awesome start. If you are using Rails > 5, you already have modern and nice dependency on concurrent-ruby library as well. Check it out, and use it - it can simplify your design greatly.
There is a good reason to avoid basic threading primitives provided by Ruby, however. They are mapped directly from underlying UNIX system calls, and are neither easy nor safe to use directly by a novice programmer. Don’t use it to prototype things, grab a nicer library and iterate if you need to improve performance later.
Alternative deployment options
While working with Ruby code, I used to default to go with Heroku, and if this proven to be difficult or pricey later on - I’d switch to Capistrano and more traditional server. While this is relatively simple and works in most cases, the approach has significant drawbacks.
Deployment options in Elixir are way more complex, and overwhelming at the beginning. I actively avoided learning how to deploy Elixir apps for embarrassingly long period of time.
In principle, Elixir applications are compiled to a binary, that is being uploaded to server and started there. This differs greatly from traditional Ruby deployment, where you’d send the source code to server, build your dependencies and run it there.
You may say that the difference is in the fact that Ruby is interpreted and Elixir is compiled - and you are right. The thing is, our applications are no longer Ruby only. There’s plenty of JavaScript, SCSS files, graphics files that need to be processed by assets pipeline too. This does not have to be done on the server.
You need all of the build dependencies on the server or Heroku instance
if you want to go with traditional deployment route. Contrary to that,
if you use something like Docker for Rails
deployments,
your build phase is performed on your local machine, and the thing you
push to Heroku or a VPS is already ready to run. You will still need
node.js
on the server, since Rails uses it internally, but you don’t
need to deal with NPM packages, versions, dependencies etc. This makes
maintaining servers with Rails applications easier in long run.
Outro
A developer who is actively trying to improve their skills, needs to reach out to other languages, environments, communities. You may stay using the same stack you’ve been using for years, but it does not mean the experience of learning new technology is completely wasted. Consciously or not, you will import some of the new things you learned to your every day workflow - and become a better developer in the process.
I did this to myself, and then propagated the knowledge to the team. In fact, we organized a series of internal workshops about Elixir, and the developers taught the stack themselves, hands-on. It’s probably a subject for a separate blog post - but I believe we are all now better Rubyists. Do that to yourself and your Ruby development team too.
Now stop reading and go teach yourself some Elixir. It’ll make you a better Rubyist.
Post by Hubert Łępicki
Hubert is partner at AmberBit. Rails, Elixir and functional programming are his areas of expertise.