Reinventing Objects
Table of Contents
In the book “Grokking Simplicity”, the author Eric Normand emphasizes that you should clearly separate data, calculations and actions. Data is probably the simplest to understand: anything you load from a database, file or external system in general, including incoming requests and so forth. Calculations are another name for pure functions: these functions only produce an output and do nothing else. Actions actually do side effects and therefore make the program useful. In functional programming, you try to maximize the amount of code in calculation because these functions compose well and are easy to reason about. This is not to say that functional programmers are afraid of side effects. Rather, the actions where side effects can happen are called explicitly; as explicit is the handling of potential errors.
This is somewhat contrary to OOP.
The client of an object does not want to know what it takes perform an action or even if it involves side-effects at all.
When you call getProduct
on an object, you’re telling it: Get me that product data, whatever it takes!
This insight has led me to a train of thought I want to share with you today. I call it the Reinvention of Objects.
Starting with functions
The most basic building block for any useful program is a pure function. Pure functions are awesome! They are incredibly easy for the programmer to write and test, and are likewise easy for the compiler to optimize. Caching the result of a pure function is trivial and parallelizing pure functions is a no-brainer. Imagine you are building a web service that converts from one unit of measure to another. Given an easy-to-use web framework, you can already build a useful service only with pure functions, as our service for unit conversion just takes an input request and produces an output response.
But of course, there comes the point where pure functions don’t cut it. For example, if we want to convert US dollars to Euros, we need to get the current exchange rate from somewhere. Let’s say there is an API that we can use to get the value. Now, our unit conversion code changes from a pure function to a side-effect conducting action because performing an HTTP request is a side-effect. We now need to deal with the possibility of a network error, but this shouldn’t be too hard as long as the program logic is still simple. Adding some error mapping or default value should cover it.
Injecting parameters
Imagine that the API used to get the current exchange rate requires an API key to be sent along. We do not want to store the key in the source code, so it needs to be read from somewhere like an environment variable or a config file when the service starts. Again, we introduce the possibility of errors, but we also have another problem: How does the API key, once it is loaded into memory by whatever mechanism we chose, end up as a parameter to our business code? We could pass the key around from the main function down to whatever function needs it, but this might be tedious. Moreover, we do not want our business code to know that there even is such a thing as an API key. It is a technical detail that we’d prefer to keep out of sight unless we actually care about it.
The functional way to do this is to use currying and partial application to inject the key into the function that makes the API call. This means that the number of parameters of the original function would be reduced by one. Surrounding code would not know anything about API keys, only units of measurements and so on, as it should.
The OOP way to deal with this problem is to create an object that, when constructed, receives the API key as a parameter and encapsulates it. As with the functional approach, the client code does not know about API keys, as it should.
Local state enters the scene
So far, we have assumed that the API key is valid all the time. But there might be scenarios where this is not the case. Imagine that we have credentials that can be used to get a temporal API token that is only valid for an hour or so. Our API-calling logic would need to first get the token, store it, and use it for any request that occur within the validity time of the token. Once the token is expired, a new token has to be acquired.
The functional approach of currying and partial application does not work anymore. Personally, I think this is where purely functional languages like Haskell lose most developers. It becomes unreasonably hard to deal with Monads and similar concepts for something that is incredibly simple in imperative code. Some languages, like OCaml or F#, take a practical approach here and offer an escape hatch out of the functional world by allowing mutable variables that behave just like in imperative languages most programmers are familiar with.
For the OOP approach, the facade that the clients see from the outside does not change and the code inside the object is still fairly simple. It should also be fairly easy to switch from an object to an actor: For example, let’s say we want to refresh the token as soon as it expires. An object can only make the necessary API call when it is invoked by an outside client. On the other hand, an actor can make this decision without any interaction, as it is an independently running process. In a way, both objects and actors act like a code representation of an external system that other parts of the program can use. If we started with objects to begin with, all other changes to the API interaction we introduced along the way (except the error handling) would not have affected the client code.
So objects are better?
Earlier, I have stated that functions, especially pure functions, are awesome and then provided an example where their simplicity breaks apart. However, do not think that I take OOP as the superior paradigm. I believe there is a nuance to find here.
Objects and actors make a lot of sense when you want to interact with the outside world, as it always involves some localized state. This might be an API token, or a file pointer, a cache or some sensor value. Alan Kay’s original idea of OOP, where objects are like little servers that we can send and receive messages with, encompasses this perfectly. Objects are also great for some internal components of a program, like a database connection pool or a request router.
However, the complexity from objects increases when we try to model everything as an object and when even simple tasks require the composition of multiple objects that have nothing to do with other parts of the program that justify some sort of coordination. I do not think that we should create sorter objects. Sorting should be implemented as a pure function. I also do not think that a customer’s shipping address is an object in the sense that Alan Kay coined the term. An address is just data, a bunch of key/value pairs that we pass around in the program. And I have yet to see a single reasonable example of inheritance.
What I do think is that most programs should only consist of a handful of objects or actors, most (if not all) of them singletons created at application startup and available globally to all parts of the program. Individual use-case implementations you typically find in web servers, like “add item to shopping cart” or “mark TODO as complete” can best be implemented as imperative procedures that receive data, transform and make decisions with it using pure functions and reach out to objects or actors to do something useful before returning some other data back to the client. CLI tools are often even simpler, as there is no need to spin up objects like little servers if they will be shut down immediately after one use.
Concerning Java
When I started programming back in high school, the only language I knew was Java. After trying out a few other languages for a short while and working with TypeScript as our project’s main language for over two years, I must say that Java’s interpretation of OOP seems somewhat absurd to me now. Everything looks like a nail if you hold a hammer and everything is an object if you know Java. And I don’t think I am alone with this assessment, since other languages are gaining more attraction. I am observing the development of Java with curiosity, but even more so the development of the JVM ecosystem and alternative languages for the JVM, like Kotlin.
Conclusion
We’ve looked at how functional programming and OOP paradigms approach a common problem and have seen that pure functional programming can become cumbersome when requirements change. We have also seen that OOP might provide a better facade to clients, but involves a different set of problems that need to be tackled.
Hopefully you found this blog post helpful. It reflects my current interpretation of functional programming and OOP and where they fall short. I have been thinking about this for quite a while now and hope my reasoning from this blog post can help you evaluate your use of functional programming and OOP in your own projects. Feel free to comment if you have a similar or different opinion or would like to know more.
Comments powered by Disqus