Code faster by writing your code as an RLW (an original acronym introduced in this piece that stands for Read, Logic, Write) using the RLW template. RLWs are faster to code than other code because they’re faster to test and less coupled. Faster testing means faster debugging which means faster coding. And less coupling means less communication which means faster coding.
Non-RLW
Consider the following code that’s not an RLW. It’s fast to write and well structured, although slow to unit test and debug. It’s short, 38 lines, and solves a simple algorithmic programming problem from my apprentice Chris Liu.
#!/usr/bin/env python3
import sys
def is_s_int(s):
try:
int(s)
return True
except ValueError:
return False
def main():
t = int(sys.stdin.readline())
for i in range(t):
n = int(sys.stdin.readline())
current_answer = ord(sys.stdin.readline()[0])
for j in range(n-1):
relative_position = sys.stdin.readline().strip().split(" ")
if relative_position == ["SAME"]:
pass
elif relative_position == ["VERY", "LEFT"]:
current_answer = ord("A")
elif relative_position == ["VERY", "RIGHT"]:
current_answer = ord("E")
elif relative_position[0] == "LEFT" and is_s_int(relative_position[1]):
current_answer -= int(relative_position[1])
elif relative_position[0] == "RIGHT" and is_s_int(relative_position[1]):
current_answer += int(relative_position[1])
else:
raise Exception(f"Invalid relative_position : {relative_position}")
print(chr(current_answer))
if __name__ == "__main__":
main()
RLW
Now, let’s rewrite it using the RLW template. It’s fast to write, well structured, and unlike the previous code, fast to test and debug. The overhead for using the RLW template over not using it is 14 lines, bringing the code to 52 lines. This sounds big, but, the RLW template a fixed overhead, so if the program were 1000 lines long, the overhead would still be around 14 lines, which is negligible.
#!/usr/bin/env python3 import sys def R(): return sys.stdin.readlines() def L(input_data): def is_s_int(s): try: int(s) return True except ValueError: return False lines = [line.strip().split(" ") for line in input_data] t = int(lines[0][0]) row_index = 1 for i in t: n = int(lines[row_index][0]) current_answer = ord(lines[row_index+1][0]) for relative_position in lines[row_index+2:row_index+n+1]: if relative_position == ["SAME"]: pass elif relative_position == ["VERY", "LEFT"]: current_answer = ord("A") elif relative_position == ["VERY", "RIGHT"]: current_answer = ord("E") elif relative_position[0] == "LEFT" and is_s_int(relative_position[1]): current_answer -= int(relative_position[1]) elif relative_position[0] == "RIGHT" and is_s_int(relative_position[1]): current_answer += int(relative_position[1]) else: raise Exception(f"Invalid relative_position: {relative_position}") row_index += n+1 output_data += chr(current_answer) return “\n”.join(output_data) def W(output_data): print(output_data) def main(): input_data = R() output_data = L(input_data) W(output_data) if __name__ == "__main__": main()
What makes an RLW, an RLW?
In the non-RLW, reads, logic, and writes are intertwined throughout the code. But in the RLW, all reads are done up front in R, all logic is done in L, and all writes are done at the end in W. These three sections are also explicitly broken out into stand alone methods: R, T, and W. Hence the name, RLW.
What makes RLWs fast to code?
Mostly faster testing, with a side helping of improved decoupling.
Testing
Testing is where RLWs really shine. Writing tests for RLWs is really fast. And, since testing and debugging is the slowest part of development, faster testing which leads to faster debugging improves your speed.
Think for a second about how you’d test the non-RLW code. Well, it has reads and writes littered throughout the code. So, the fastest way would be an integration test framework that tests unix commands. Okay, not so bad. But let’s say you want to write a unit test. Then to pass test data, you’ll need to mock out sys.stdin using pytest’s monkey patching. Then, to determine if a test passes, the test will have to check what’s written to sys.stdout. More monkey patching. At this point you’ve built and must maintain a custom testing framework. Sounds slow. Now imagine if this code was more complex. Collating data from databases, APIs, environment variables, user input, etc. Now your testbed has to become more complex and intricate to handle all of the mocking. It became a codebase in its own right. Just to write unit tests. This sounds like a slow way to write unit tests.
Now think about how you’d test the RLW code. Since the code is the same on the outside as the non-RLW test, integration testing is exactly the same. But what about unit tests? All of the logic is in L, so we can test RLW by testing L. How do you write a unit test for L?
assert test[“expected_output”] == rlw.L(test[“input”])
No mocking. No harnesses. No maintenance. No matter how complicated the IO.
All that’s left is to create the test data.
Unit Testing R and W
What about testing R and W? Well, given how simple they are, what’s there to test exactly?
def R(): return sys.stdin.readlines() def W(output_data): print(output_data)
You’d basically end up testing readlines and print, and there’s no need to unit test standard libraries.
Parallelizable Coding
RLWs are coded faster in parallel (i.e. coding with multiple people) than their non-RLW counterparts. Parallel coding speed depends on the amount of communication required to code which depends on how coupled the code is. If Joe’s code change is coupled with Bill’s, that’s either a merge conflict, a meeting, or a bug. All outcomes slow down Bill and therefore the whole team. But RLWs are decoupled into R, L, and W, so, each can be coded parallel without communication. R and W are easily parallelizable within themselves. The optimum is one person per reading source and one person per writing source. In our example, R and W were trivial. But in practice, reading and writing to multiple non-trivial interfaces can be parallelized by assigning work per interface. L is just logic, which means it’s functionally pure, and functionally pure code is less coupled than impure code, and therefore also coded faster in parallel.
Pure or Simple, mostly
Code that’s not pure and not simple is much harder to write than code that’s pure or code that’s simple (not to mention less parallelizable). When working with it, you have to keep in mind both the logical complexity within the code as well as the external complexity outside of the code. Code that requires a lot of knowing and thinking is slow to write. That’s no good.
In an RLW, the only method that is both not simple and impure is main(). But, at any other point in the code, you’re working with pure code or simple code. That’s less to think about at any one that, and less thinking means it’s faster to write. That’s good.
The RLW Template
To write an RLW, use this template and fill out methods for R, L, and W.
#!/usr/bin/env python3
def R(): # “Reads” from outside sources into input_data. # Cannot contain any complicated transformation logic # Cannot contain any writes def L(input_data): # “Logically” manipulates input_data into output_data. # Cannot contain any reads # Cannot contain any writes def W(output_data): # “Writes” output_data to outside sources. # Cannot contain any complicated transformation logic # Cannot contain any reads def main(): input_data = R() output_data = L(input_data) W(output_data) if __name__ == “__main__”: main()
When you can’t use an RLW
RLWs are fast to code, but not all code can be written completely as an RLW. Luckily, even if the entire program can’t be written as an RLW, most of it can, which means most of the code will be fast to code.
So, for example:
Interactive programs cannot be fully written as an RLW
But, the event processor can be an RLW
Daemons cannot be fully written as an RLW
But, the event processor can be an RLW
Code with conditional reads break the RLW pattern because there’s logic before the read.
But, everything after the conditional read can be an RLW
Write failures, which involves reading and logic after a write, break the RLW pattern
But, everything before the write failure can be an RLW
Summary
As we can see, most code can be written as an RLW, even if there are problematic sections. And, code that is written as an RLW is fast to code because its of faster testing and complexity decoupling.
Interesting and useful! Does this "pattern" go by other names? The closest concept I have been able to track down is pipes and filters in Pattern Oriented Software Architecture.
Interesting post - seems like it is a step on the way to functional programming, which has additional benefits.
It would be great if you could use a smaller font for code samples so that lines don't wrap - hard to read the code. Syntax highlighting would be nice but less important.