Functional Programming Techniques With Ruby: Part I
Ruby is an interesting language, in that it supports the use of multiple paradigms. One of these is the “functional paradigm”.
Features of Functional Languages
Using a language in a functional style implies you have access to a few key features:
- Immutable values: once a “variable” is set, it cannot be changed. In Ruby, this means you effectively have to treat variables like constants.
- No side-effects: when passed a given value, a function must always return the same result. This goes hand in hand with having immutable values; a function can never take a value and change it, as this would be causing a side-effect that is tangential to returning a result.
- Higher-order functions: these are functions that allow functions as arguments, or use functions as the return value. This is, arguably, one of the most critical features of any functional language.
- Currying: enabled by higher-order functions, currying is transforming a function that takes multiple arguments into a function that takes one argument. This goes hand in hand with partial function application, which is transforming a multi-argument function into a function that takes less arguments then it did originally.
- Recursion: looping by calling a function from within itself. When you don’t have access to mutable data, recursion is used to build up and chain data construction. This is because looping is not a functional concept, as it requires variables to be passed around to store the state of the loop at a given time.
- Lazy-evaluation, or delayed-evaluation: delaying processing of values until the moment when it is actually needed. If, as an example, you have some code that generated list of Fibonacci numbers with lazy-evaluation enabled, this would not actually be processed and calculated until one of the values in the result was required by another function, such as
puts
.
Using Ruby as a Functional Language
In this series, we will visit each of these features one by one, and look at how you can use them to produce cleaner, more efficient, and better structured Ruby code.
I should point out in advance a warning: using some of the techniques we will cover in production code is not recommended. I’ll try to clearly state, whenever I can, which features these are and why they are bad. The fact is, though, that Ruby is, at heart, an OO language for an OO world. Functional style code can be written in Ruby easily, but it suffers mostly from performance problems as a result of not being the focused on the paradigm.
More on that later; for now, let’s talk about the topic at hand: immutability.
Immutability, and Side-Effect Free Code
To start the series off, let’s explore immutability and side-effect free code. Pure functional languages do not allow mutable data, and as a result, functions cannot do anything but take arguments and produce a result using these arguments.
What Is Side-Effect Free Code, and Why Is It Good For Me?
Functional programming is a paradigm based mostly on the principles that programming should follow the rules of mathematics. Let’s take the following, valid imperative programming paradigm expression:
x = x + 1
If we apply the rules of maths to this, we should be able to rebalance the equation:
x - x = 1
∴ 0 = 1 (!?)
Woah, what happened there?! Clearly, this doesn’t work. The imperative style of programming doesn’t match the mathethematical underpinning of functional programming. Mathematical functions are built on the idea of immutability; that is, values cannot be redefined like we have done in the imperative statement above. We can combat this problem by using a new constant to represent the updated value:
x = 1
y = x + 1
Now this is perfectly valid in both paradigms:
y = x + 1
∴ x = y - 1
The functional programming paradigm, with its roots based in mathematics, uses this basic idea at its core. When using Ruby for functional programming techniques, and you want to try to avoid side-effects, you should endeavour to never redefine a variable; that is, treat variables as constants. There are numerous benefits to this approach, some important ones of which are:
- Functions, or methods, become easily unit tested. This is because, if they never mutate data, but rather only ever provide copies or new objects as return values, the same input is guaranteed to always produce the same output. Thus, the code can be said to be “side-effect free”.
- Your code becomes incredibly concise and readable. As you will have to create a new variable for every mutation you need to do, it will be obvious at first glance as to the scalability and shortfalls of a particular method. Pure functional languages often tout the easy ability to reason about performance of a given block of code as a feature.
- Chaining becomes incredibly easy, mainly because what is returned by every function is a new, immutable value that can be passed into the next without worry, and discarded without concern.
- Your functions will be domain free; you can apply them in an infinite number of ways, as they have no state specific logic hidden inside of them.
Writing Side-Effect Free Code
Version Number Parsing
In 2010, Yahuda Katz posted a simple, but interesting question on Twitter1 :
Question: what’s the easiest way to get [“X::Y::Z”, “X::Y”, “X”] from “X::Y::Z” in Ruby?
Of course, there are dozens of solutions to this problem, but solving it in a functional style proved to be a lure too good to resist for the Ruby Quicktips Tumblr 2:
def module_split(module_path, separator = "::")
modules = module_path.split(separator)
modules.length.downto(1).map { |n| modules.first(n).join(separator) }
end
module_split("W::X::Y::Z")
There are many benefits to writing this method in a functional style with zero side-effects:
- Easily testable with unit tests.
- Uses as many core Ruby methods as possible, decreasing the likelihood of error creep and wheel re-invention.
- Pretty fast. Uses memoization where possible, and optimised Ruby core methods elsewhere.
- Clean, readable, easily understood; it virtually reads like plain English.
Fluent DSLs With Chainable Methods
One major benefit of using a functional style is that it generally makes chaining a piece of cake. In this example, we’re going to try and build a DSL for building up a block of CSS code; we’ll build up a selector, along with properties and values to set styles on.
As such, we need a class, with two methods: one for adding properties, and one for setting the selector.
class CssBlock
# We'll add some methods in here.
end
First, we’ll set the selector as a mandatory property in the initialiser. We’ll also set up the properties attribute as an empty Hash
:
class CssBlock
attr_reader :selector, :properties
def initialize(selector, properties = {})
@selector = selector.dup.freeze
@properties = properties.dup.freeze
end
end
Note that we’ve used an attr_reader
for both properties, and we’ve duplicated and frozen the values; now we can guarantee the passed in values cannot be modified.
Next, we create the code to add some properties to the block:
class CssBlock
# ...
def set(key, value = nil)
new_properties = if key.is_a?(Hash)
key
elsif !value.nil?
{
key => value
}
else
raise "Either provide a Hash of values, or a key and value."
end
self.class.new(self.selector, self.properties.merge(new_properties))
end
end
The key takeaways from this block of code are:
- We accept either a
Hash
of properties to merge in, or a key/value pair to add to the already existing properties. - We return a new instance of the
CssBlock
class, setting the properties to be the merge result of the existing properties and our new properties. - We raise an error if they didn’t give us the right format of items.
- We do not add, edit or modify any state stored on the copy of the object we’re dealing with. Once the object is instantated, that’s it; it’s fixed as it was.
Now, let’s add a method to serialise the contents of this class, which is basically us just defining the #to_s
method:
class CssBlock
# ...
def to_s
serialised_properties = self.properties.inject([]) do |acc, (k, v)|
acc + ["#{k}: #{v}"]
end
"#{self.selector} { #{serialised_properties.join("; ") } }"
end
end
We use #inject
to build up an array of stringified properties. One absolutely crucial little detail here is that we do not modify the acc
variable within the block. As Ruby’s equivalent of the fold/foldr
function found in most functional programming languages, #inject
is a staple method for building up objects in a functional way as shown.
Now to test it out:
CssBlock.new("#some_id .class a").set("color", "#FFF").set({ "color" => "#000", "text-decoration" => "underline"}) # => "#some_id .class a { color: #000; text-decoration: underline }"
Huzzah, it works! Not only that, but it’s super simple, easily testable, and should prove to be very easy to work with or extend as necessary.
For your convenience, here’s the full source code:
class CssBlock
attr_reader :selector, :properties
def initialize(selector, properties = {})
@selector = selector.dup.freeze
@properties = properties.dup.freeze
end
def set(key, value = nil)
new_properties = if key.is_a?(Hash)
key
elsif !value.nil?
{
key => value
}
else
raise "Either provide a Hash of values, or a key and value."
end
self.class.new(self.selector, self.properties.merge(new_properties))
end
def to_s
serialised_properties = self.properties.inject([]) do |acc, (k, v)|
acc + ["#{k}: #{v}"]
end
"#{self.selector} { #{serialised_properties.join("; ") } }"
end
end
Caveats of Side-Effect Free Code In Ruby
The reality of trying to write side-effect free code in Ruby is that it doesn’t really work for much else but experimentation at this stage. The raw truth of the matter is that Ruby has several major problems handling properly immutable data:
- Freezing objects cannot be automated. You can get part way by monkey patching the
#new_method
on Class to call freeze on the return value of the original#new method
, but that method doesn’t get called when certain core types are instantiated (Hash, String, Array, etc.). That means you’ll have to manually freeze objects if you want them to be forced immutable; if you don’t, you can’t guarantee you’re actually writing side-effect free code. - The steep cost of object creation in Ruby means that using a functional style of programming is going to prove costly to any performance related goals. Hopefully, this problem will fade away as Ruby implementations push for faster interpreters.
- One of the key benefits of functional programming, simpler concurrency, is difficult to achieve in Ruby due to the presence of the GIL and a naïve threading implementation. In alternative Ruby implementations such as JRuby and Rubinius, this is not such a big problem. Although Ruby 1.9 has made big strides towards a better threading model with Fibers and such, it will still make fully exploring this realm of benefits harder than it is in a purpose built functional language as there is still no way to have proper parallel threads in CRuby.
Whilst these techniques are not necessarily useful in production code, they are extremely useful to keep in mind when designing robust object models.
Next Time…
We’ll talk about higher-order functions, and how they allow currying, a very interesting and highly useful programming approach.
1: http://twitter.com/wycats/status/9042964562
2: http://rubyquicktips.com/post/1018776470/embracing-functional-programming