With all the fun and benefits of FP I’ve come to know in JS, it’s hard not to import some of those patterns into Ruby. In the same way people might pass time doing sudoku or crossword puzzles, I like working through exercism.io challenges in a variety of languages, so I’m always faced with new opportunities to write fresh Ruby with a functional twist.
Recently I completed a Ruby implementation of the Luhn algorithm. Right away I saw it as a pure function: take some text and return a boolean (
String -> Bool). In more spartan languages like JS (with ES6) and pure functional languages, this could be done with very few lines and characters, but the opinionated Rubyist in me and the structure of a PORO (plain ol’ Ruby object) require a bit more boilerplate and ceremony.
My goal is to write Ruby in a functional way that that still respects and celebrates the idioms of my beloved first language. Here was my first pass at the Luhn algorithm challenge:
Don’t worry too much about the implementation. Just know that it interrogates a string input and returns
true if the string is valid and
false if not.
I was happy enough with the solution, but I didn’t love how repetitive
def method_name(raw) was. What choice did I have though if I want to write pure functions that only take inputs and return outputs?
My high-level goal is to check the validity of an input string based on different sets of criteria: length, character types, and a calculated sum based on the Luhn specification. In each method that takes
raw as an input, I’m only reading it to calculate some new value. What if there was some built-in machinery in Ruby to let me easily read values without passing them into methods?
Right! Every Rubyist’s friend
attr_reader ! On my second iteration I used an
attr_reader value instead of passing
raw to everything and removed the singleton class in favor of one of my favorite patterns in Ruby.
All my private helpers reach out of their scope for
sanitized , so it seems pretty non-functional at first. But when I really look at those functions, they are private methods that will always return the same value given the same value of
sanitized. In this way, it’s as if
sanitized has just been partially applied to all these helper methods, and they just maintain access to a closure scope. I can be 100% confident in their behavior as long as
sanitized stays read-only.
While it’s a lot more code than I might need in a JS implementation, I think it reads really well. It is very declarative up top, making you only dive deeper if you really want to. I originally used the
Enumerable#all? pattern because I hated looking at
valid_length?(raw) && only_digits?(raw) && valid_sum?(raw)
It’s not too crazy, sure, but having to repeat that argument so many times made me just feel weird. I think this second pass is a nice blend of classic Rubyisms and functional flare. Let’s review the two:
Rubyisms and PORO Elements
- Class method to instance
def self.method; new.method; end
- Every Rubyist’s favorite
?suffixes for predicate methods
- Composition/chaining in
- Variable names of
x🙀. I’m cool with this because I am obviously doing math so the vars are obviously math-able values (aka numbers). In others words, I don’t think naming it
number, or even
intadds much more meaning/intent than
- Declarative by default. The assumption is that future developers only need to know what
Luhn#valid?does, not how it works. If the developer wants to understand the implementation, they are free to do so, but not required. It would be totally adequate to just look at the definition of
Luhn#valid?to get a good enough understanding of what is going on.
How long will my quest for the sweet spot of functional programming and Ruby (fRuby?) go on? Who knows. But using
attr_reader as a roundabout form of partial application is a pleasant example of sticking to Ruby conventions and adding a very disciplined intent informed by concepts such as immutability and purity.
Ruby is fun to write. Functional code tends to run very well and is easy to reason about (given an understanding of the core principles). Reasonable an well-running code is also fun, so I think there is plenty of room to keep looking for ways to add functional flare to the Ruby community.