Here are some of the main advantages I see to doing things in a functional way:
- Less code – writing simple, small functions allows you to reuse them in many different places by giving you the flexibility to compose and combine them in any way you can think of. This leads to fewer levels of abstraction, less duplicated effort, and more code reuse.
- Simpler and more robust tests – when functions do only one thing, they’re very easy to test. Second, by using functions, you make it very clear what goes in and what comes out – which offloads a lot of work onto the compiler. This means that you can have more confidence that your code works just by the fact that it compiles. This allows you to write fewer tests, and as a result those you do have will be more robust because they’ll be testing the important things (rather than every little thing that could go wrong).
Here are the main disadvantages I see to using a functional style in your code:
- Notation can be difficult to grok – Prelude is one of my favorite Swift functional programming libraries… however, my interns have struggled with the notation and knowing what everything does. Passing around functions, once you get used to it, is pretty understandable, but it can be a bit of a learning curve to get there. I also worry that it can be easy to fall in love with how clever your code is with FP, and as anyone who’s had to debug someone else’s “clever code” can tell you, that can be a huge downside.
- Organization and SRP – as opposed to OOP, FP can often be pretty flat. Defining tons of small, simple functions at the top level means that there will be tons of those in your autocomplete. You have to be careful to make sure all those beautiful functions don’t become undiscoverable simply because there are too many. Similarly, grouping those functions can be a bit of an adventure, and raises questions about the single responsibility principle with respect to the files that hold them.
FP gives you a lot of power for little work, which is a double edged sword: it can be easy to get carried away. So here are some ways to lean into the good parts, and avoid the bad parts.
Build data pipelines
I often tell my interns that apps are just fancy instructions for turning JSON into pretty pictures. While that’s a vast simplification, it can help to think of things in that way. So, I recommend building reusable pipelines to transform server data into views. This makes it easy to visually test your entire pipeline, from server to screen.
Let’s look at a simple example. Suppose we want to display a list contacts. A nice way to do this in a functional style might be to create a series of functions that do one part of the process, and then string them all together. If you’re using my library for simplifying UITableView logic, you might create:
- a function to turn NSData from the server into a list of contacts (let’s call this function
parse
) - a function to configure one UITableViewCell subclass with one contact model (and call this
configureCell
) - a function to turn a list of those configurators into a table data source (call it
configuratorsToDataSource
)
Then you could build a pipeline from these pieces by composing: a) parse
, b) a map over the contact models using configureCell
, and c) configuratorsToDataSource
.
The nice thing about this approach is you can test each piece both separately and together with a variety of different tools (unit tests, of course, but also UI tests and playgrounds that display the output). This will give you confidence in these functions, and allow you to only worry about the two ends of the pipeline (what goes in and what comes out). Incidentally, if you use this breakdown of the task into small functions, it’s nice even when you use those functions imperatively:
func applyPipeline(to data: Data) -> UITableViewDataSource { let contacts = parse(data) let configurators = contacts.map(configureCell) return configuratorsToDataSource(configurators) }
But it is certainly very clean and elegant looking if you use the composition operator >>>
to pass the result of one function into the next:
let pipeline = parse >>> map(configureCell) >>> configuratorsToDataSource
You can then just call this with pipeline(data)
.
Test your network calls
This may seem like a no-brainer, so let me explain what I mean here.
In the last section, we said that pipelines allow you to only worry about what goes in and what comes out; so, you need to be careful about what goes in, and that usually means your network stack.
To make this easy, I like to create files in my test target that hold json that the server returns. Anytime the server changes the format of that json, I can just drop that new json into the appropriate file, run tests, and see what breaks. The key here is to make these unit tests really dumb: just stub the calls with the files, and test that the models have the properties you expect. No logic here, just: do we get everything we need from the given json?
Y tho?
Testing network calls gives you confidence that what goes into your pipelines is correct; along with your pipeline tests, you’ve reduced the set of likely bugs to just layout issues.
Minimize/isolate/explain custom operator use
Readability is arguably the most important aspect of any codebase. If code is readable but incorrect, it’s easy to fix. If it’s slow but readable, you can speed it up. But if it’s NOT readable, then it will be extremely difficult to make either correct or fast.
So, how do you make functional code readable?
This is more of a challenge than you might think. Ostensibly, all the custom operators make pipelines easier to understand – but only as long as you know what the operators do. Consider the example from the first section. With operators from Prelude it might look like:
let pipeline = parse >>> map(configureCell) >>> configuratorsToDataSource
As long as you know that the >>>
operator means composition (namely, the result of the lefthand function is the parameter of the righthand function), then it’s very clear what this does. It’s certainly easier to read than this:
let pipeline = { configuratorsToDataSource(parse($0).map(configureCell)) }
However, a beginner who looks at the first gets very confused, whereas the second, while gross, can be reasoned out. And this only deals with >>>
, and doesn’t even getting into any of the more advanced operators!
For this reason, I tend to confine my operator use to a small subset of what’s out there, and as much as possible that just means |>
and >>>
.
I also try to put these things in separate files, so that if it becomes too unreadable, I can swap it out easily without affecting the rest of the application. Along with the tests on both the pipeline elements and the pipeline in toto, I can refactor that code with confidence.
Finally, there may be times when unusual custom operators are the best choice for the job. In that case, I turn to the age old solution for such code: commenting. Just explain what you’re doing, and perhaps where someone reading it could go to find more info.
Namespace your top level functions
If you’re not careful, you could end up with hundreds, thousands of functions at the top level, which, to say the least, is a mixed blessing. Before that happens, I recommend namespacing your functions so you can still find them all. There are several ways to do this.
Static functions
One way to namespace is by wrapping the functions in a class or enum and marking the function as static. For instance:
class ApiRequests { static func login(_ email: String, _ password: String) {...} }
or as an enum. The advantage of the latter is that you can never create an instance of the type, so all consumers of the function will know this is purely for namespacing purposes. You can then call it like this: ApiRequests.login(email, password)
.
Manually
Another way is to simply prefix the name of the function with a standard set of characters, for instance:
func apiLogin(_ email: String, _ password: String) {...}
I tend to like this for being more succinct, and having a way you normally do this can make things a lot more discoverable (e.g. just start typing the prefix of the thing you want and you have a list of everything you can do in that subject area.
Frameworks
If you really want to get drastic, you can put your functions in different frameworks altogether. This has some advantages when it comes to testing, and forces you to make your code more modular. On top of that, in case of naming collisions, you can refer directly to your framework:
MyFramework.String.init(...)
Conclusion
To keep functional programming working for you instead of against you, keep these simple things in mind:
- Build well tested pipelines from the server to your views
- Test your network calls
- Don’t get too clever with your notation – or else, explain it with comments as well
- Namespace your top level functions