I am a firm believer that we have to learn from the past instead of throwing all away for the future. And this is one of these moments: I have seen multiple talks, presentations, slides telling about SOLID and Design Patterns in functional programmings. Some are serious, some are satires, but most only speaks “you just need functions, really!” on their explanation on the principles, effectively diminishing the usefulness of the principles and also by imagining that, somehow, if we only use functions all our problems on developing software would be solved forever.
So, to counter that, I’m going to re-visit SOLID, but this time I’ll not compare with “OO” – instead, I’ll ask for us to try to grasp the meaning behind each principle. I’m going to use the definitions from Wikipedia, because (1) it’s easier to track changes and (2) it’s condensed from multiple sources, so it’ll probably not reflect a single opinion from an author. I also thought about being one single post, but it became quite complicated, so I’m splitting this post in multiple ones (and probably the one about LSP will be the most extensive of all). So, let’s begin by the first: the Single Responsibility Principle:
The single responsibility principle is a computer programming principle that states that every module, class, or function should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class, module or function
Some authors interpret that this means: a piece of code should have only one reason to change. In the functional world, this can mean “please, separate your IO from your logic”. So, for example, instead of doing:
(defn mean [a b] (println "Mean:", (/ (+ a b) 2.0)))
it’s better to separate the printing and calculation on two functions. Now, this sometimes is easier said than done. Consider the following fragment from Chlorine:
(defn inline-result [^js editor row parsed-result repl] (let [parsed-ratom (render/parse-result parsed-result repl) div (create-div! parsed-ratom) inline-result ^js (get @results [(.-id editor) row])] (.setContent inline-result div #js {:error (-> parsed-ratom meta :error)})))
Here I am “parsing a result”, “creating a DIV”, “getting an element from a Clojure atom”, and “rendering the content”. Is this one thing, or multiple? The thing is, there’s only one reason for this code to change: if I want to render results differently.
But, to be honest, even I don’t know if it is doing only one thing or more than one (considering that I have multiple side-effects, like creating an HTML div, rendering things on that div, getting a data from a mutable repository, and rendering it all on the screen). Maybe I could change the function to make “only one thing”, but it is not clear what this would mean on this context, so I’ll not change this function, for now.
From people that came from Haskell language, they probably are now saying “that’s what we’ve been telling you for years! That’s why IO
monad exists!” and they are not wrong. But the principle is more complicated than that: it is completely possible to read from a file, try to parse it as JSON, get an exception and then return a “default object” in the same function – and it typechecks.
So, one of the things that we could do is to separate each part of the code in each specific function:
(defn parse-result [string-result] (-> string-result parse-json coerce-schema)) (defn read-file! [file-name] (try (read-file file-name) (catch Exception _ default-empty-obj)))
Now, it may not look like it, but this was my third try. Why? Because of balance:
- I could catch the possible exception to parse-json and coerce-schema. But then, I would have two reasons to change the
parse-result
function: if I somehow want to get another “empty object” in other part of the program, for example, or if I really want to raise an error if I indeed send a string result that, in that part of the program, should be a valid JSON in the valid format - I could separate
read-file!
into “read” and “capture the exception”, but then it would be just _unnecessary function wrapping__
So, again: it is not that easy as it seems…
1 Comment
Matheus Moreira · 2019-10-07 at 07:15
One point that we should keep in mind is that eventually, we’ll have a function that handles “a lot” because that function is the orchestrator: the one which calls other functions in a way that makes business sense. In this case, we could understand that this function has only one reason to change: to correctly orchestrate function calls. 🙂
I think that the “functional” SRP is not just (or mainly) about IO/non-IO code, it is about the broader sense of coherent code. In this sense, FP is an excellent style to express the SRP since it favors small, focused functions that are easily combined to provide more complex behavior.
Comments are closed.