I’ll lead with my code—before and after—then follow up with an explanation.
Before Refactoring
I had a gigantic method, Perm#make for creating permanent redirects. The site I’m working on has a million or so pages, and they move around for many reasons, often outside of my control.
# @return nil if no redirect needed, else the URL to redirect to.
def make(request_uri)
%w[
...
.
_
)
.html
_section
].each do |unwanted_suffix|
return request_uri.delete_suffix(unwanted_suffix) if request_uri.end_with?(unwanted_suffix)
end
[
%w[+ _],
%w[’ '],
%w[.. .],
%w[%20 _],
%w[%E2%80%99 '],
["children-nav/", ""],
["''", "'"],
].each do |unwanted, wanted|
return request_uri.gsub(unwanted, wanted) if request_uri.include?(unwanted)
end
case request_uri
when %r{^https://oregon.public.law/ors_(.+)$}i, %r{^https://oregon.public.law/ORS/(.+)$}i
return ORS_ROOT + "/ors_#{$1}"
when /%5C/
return request_uri.delete("%5C")
when %r{/null$}
return request_uri.delete_suffix("/null")
end
# …and on and on…
end
After Refactoring
# @return nil if no redirect needed, else the URL to redirect to.
def make(request_uri)
wrap(request_uri)
.bind { remove_unwanted_suffixes(_1) }
.bind { replace_unwanted_characters(_1) }
.bind { fix_request_uri(_1) }
# Many more of these
.unwrap
end
def wrap(some_input_data)
{input: some_input_data}
end
def remove_unwanted_suffixes(request_uri)
%w[
...
.
_
)
.html
_section
].each do |unwanted_suffix|
return request_uri.delete_suffix(unwanted_suffix) if request_uri.end_with?(unwanted_suffix)
end
nil
end
def replace_unwanted_characters(request_uri)
[
%w[+ _],
%w[’ '],
%w[.. .],
%w[%20 _],
%w[%E2%80%99 '],
["children-nav/", ""],
["''", "'"],
].each do |unwanted, wanted|
return request_uri.gsub(unwanted, wanted) if request_uri.include?(unwanted)
end
nil
end
def fix_request_uri(request_uri)
case request_uri
when %r{^https://oregon.public.law/ors_(.+)$}i, %r{^https://oregon.public.law/ORS/(.+)$}i
ORS_ROOT + "/ors_#{$1}"
when /%5C/
request_uri.delete("%5C")
when %r{/null$}
request_uri.delete_suffix("/null")
end
end
# Turn Hash into a "Result" type by adding #bind. It's
# very similar to #then, with the addition of a short-circuit
# feature.
module HashRefinement
refine Hash do
def bind
# Short-circuit if we already have a result.
return self if self[:result]
# Otherwise, run the given block.
result = yield self[:input]
merge(result:)
end
def unwrap
self[:result]
end
end
end
using HashRefinement
Railway Oriented Programming turned out to fit this code very well.
At first I was pretty challenged by how to break up #make cleanly without lots of duplicated logic. The interesting part of the puzzle is the logic of trying dozens of URL patterns in sequence. Then, if one pattern matches, immediately stop and return a repaired URL.
So one issue with my code is that it was essentially a gigantic case statement: it was difficult to understand the overall logic. It was difficult to follow as I paged through it because it was one giant method with nested logic. Another issue was the requirement of repeated return statements.
Railway Oriented Programming offers a very tidy way to re-conceptualize the solution. Here’s a pic from that web page:

The author is showing three functions. As long as each function returns a successful result, it’s fed into the next in line on the green track. But if any function returns an error, the “train” is sidelined to the red error track and it stays there.
That’s a cool idea, but the real added value is: the functions stay simple and the top-level code just calls them in order. If any return an error, they’re short-circuited and the original error is returned as the final result.
So instead of dumping out of the function early, just keep going down the list of sub-functions to run. But automatically short-circuit in a very efficient way for errors. From the article in F#:
let combinedValidation =
// connect the two-tracks together
validate1
>> bind validate2
>> bind validate3
And now, translated into ruby from my code above:
wrap(request_uri)
.bind { remove_unwanted_suffixes(_1) }
.bind { replace_unwanted_characters(_1) }
.bind { fix_request_uri(_1) }
.unwrap
The really amazing part is that each of these Ruby functions required no changes: they’re each basically a chunk of code extracted from the original #make. The aspect I liked next best is that the interfaces of the parent function and sub-functions didn’t change. I.e., all the calling code still works with String = new URL, nil = no change needed.
There were a lot of ways I could go with this, and I realize that there are functional programming libraries for Ruby. I could create a real Result class. I could also somehow hide the explicit calls to #wrap and #unwrap. But it was fun and not hard to create a simple “Result” using Hash. And this new Result could easy encapsulate “success” and “failure”. The refinement keeps that change within the scope of this module. In the case of this code, failure translates to returning a quasi error string containing a new URL for redirection. And success is a nil result.
A final note: I found that Railway Oriented Programming article extremely enlightening. Along with learning Haskell, it’s given me new ways to look at solving problems.
Leave a comment