Take the word EASY: Its first three letters — E, A and S — are the 5th, 1st, and 19th letters, respectively, in the alphabet. If you add 5 + 1 + 19, you get 25, which is the value of the alphabetical position of Y, the last letter of EASY. Can you think of a common five-letter word that works in the opposite way — in which the value of the alphabetical positions of its last four letters add up to the value of the alphabetical position of its first letter?
One recent Sunday morning on NPR, I heard Will Shortz, the NY Times puzzle editor, issue the above challenge as part of his weekly puzzle segment. Like most programmers would, I immediately thought of the ways I could solve this in Ruby very easily.
But I have been slowly learning Elixir, and I’ve gotten a little bored with example programs and exercises, so I decided to solve this with Elixir. For guidance, I tried to use mostly the Elixir documentation and other references that aren’t straight up cargo-culting. (No Stack Overflow on this one.)
I’m a little embarrassed to say it took me several hours over about a week, but I’m learning and I test drove the design using ExUnit, which I hadn’t used before. So, I gave myself a break. Here is my repository with the resulting code. The basic program is on the master branch. Follow the README instructions to run it.
I felt pretty smart when I was done, and I had exercised some functional programming parts of my brain that I hadn’t before. Anyone who has done any Elixir will find the code in that repository pretty pedestrian, but for me it was a revelation.
As I was working on this I read Cameron Price’s post here on Medium about changing your patterns when moving from object oriented to functional programming. And I felt my patterns changing in tangible ways. At some points it felt like cheating — I didn’t need to maintain some object’s state anymore. I just passed transformed data structures down a pipeline until it satisfied my requirements.
So, I got it done. I even included a benchmark module so I could easily tell how long it was taking. It parses through the OSX words file of about 235,000 words, of which 10,230 are five letters long. Of those, there aren’t that many words that satisfy Will’s challenge. The code ran pretty fast — about 1.5 seconds. Ship it!
Then I got to thinking, what if going through all these five-letter words and calculating the values of the first letter versus the values of the trailing letters was a time consuming process? Maybe there’s a slow API call in there or some statistical analysis or transcoding every cat gif on the internet.
There’s more to be done here….
I work at a company with a Rails-based monolith application at its core. We have some ancillary services, also written in Rails or pure Ruby. There’s a Django app someone built once (no one currently at the company knows who or why); we treat that one like a crazy uncle who lives in the attic.
Over the past few months a couple of us have started some low level rumbling about Elixir/Phoenix. My little puzzle solver seems like a good example to show off a little bit of what Elixir might be able to do for us someday.
We have places in our apps where we have processing bottlenecks, so time to figure out Elixir concurrency and build a demo that shows off Elixir.
I have a very thin understanding that there is more than one way to handle concurrency in Elixir, but I’m a rank novice and my audience for this demo is a bunch of Ruby/Rails developers. I want it to be clear and simple. So, I set out to understand Task.async and Task.await. GenServers and such will wait for some future demo.
To simulate the long running process, I make the word scoring function sleep for a millisecond. The code for the sequential version of that code is in the “sleep-sequential” branch of the repository. I just add
Process.sleep(1) # for Elixir 1.3.0-dev
:timer.sleep(1) # for Elixir 1.2
to the ScoreWords.process_word function. Voila! The benchmark now runs in 22 seconds on my Macbook.
Now, I really have to figure out concurrency. After looking through the Elixir documentation online and reading some dense articles that I find by Googling, I figure out the correct search terms and find Saša Jurić’s post, “Beyond Task.Async” on his site, The Erlangist.
Well, I’m not looking to go beyond Task.async for this demo, but I find some helpful direction in Saša’s post. I will try to digest the rest of that article at some point. But I have some learning to do before I get it, I think.
But that’s what I needed. I just changed this in the ScoreWords module, on the “sleep-sequential” branch:
def run(words) do
words |> Enum.map(fn(word) -> process_word(word) end)
To this on the “sleep-concurrent” branch of the repository:
def run(words) do
|> Enum.map(&Task.async(fn -> process_word(&1) end))
And with that, it’s back to about 1.7 seconds according to the benchmark. That should make a pretty good demo for my team. But wait, there’s more….
This code runs so quickly that it doesn’t show off one of Elixir’s cool features: using all available processor cores. For that I’m going to need a process that runs long and puts some pressure on the CPU. For that, it’s Fibonacci time.
I won’t bore you with the details. The Process.sleep/:timer.sleep call is replaced by a call to a little module that calculates the Fibonacci sequence. I also find that I have to send Task.await a longer timeout value because I want the calculations to run longer than the default timeout of five seconds. You can see the “fib-sequential” and “fib-concurrent” branches for the details. You’ll probably want to kill them rather than try to run them to completion — I just want to show my team the CPU behavior, not that Elixir can generate Fibonacci sequences.
Watching all your cores active and your computer’s temperature spike is a beautiful thing. I hope my team finds it as interesting and satisfying as I do.
Finally, I wrote this post just to remind me of how good it felt to exercise this cool new part of my brain. It’s to help me remember what it was like to flip my mental patterns, to get comfortable working with the functional paradigm, and to believe I could actually write Elixir code without the training wheels.
The demo for my team isn’t scheduled yet. When it is, I’m going to try to impress up on them what Elixir, and functional programming in general, give us: a different way to think about problems, and a simpler set of tools to solve those problems.