How to Be Clear About What Your Functions Return
What’s in a function’s interface?
In most languages, a function’s interface has 3 main parts:
- the function’s name: it indicates what the function does,
- the function’s parameters: they show what the function takes as input to do its job,
- the function’s return type: it indicates the output of the function.
ReturnType functionName(ParameterType1 parameterName1, ParameterType2 parameterName2);
So far, so good.
But when looking at this prototype, we can notice that something isn’t symmetric: the function’s parameters have both a type and a name, while the returned value only has a type. Indeed, the return value doesn’t have a name.
In a function declaration, one could choose to also omit the names of the parameters. But still, the return type doesn’t have a choice. It can only be… a type.
Why is that? My take is that it’s because we expect the the function’s name to be clear enough to express what it returns, plus the returned value has a visible type. So a name for the returned value itself would be superfluous.
But is this the case 100% of the time?
A use case that should not exist, but that does
No. In theory it works fine but, realistically, it’s not always the case that a function’s name informs you exactly of what to expect as a return value.
Let’s take the example of a function that performs a side effect, like saving a piece of information in a database:
void save(PieceOfData const& preciousData);
And say that this operation could potentially fail. How does the function lets it caller know whether or not the operation succeeded?
One way to go about that is to make the save
function throw an exception. It works, but not everyone uses exceptions (exceptions need exception-safe code surrounding them, they may impact performance, some teams ban them from their coding conventions…). There have been hot debates and suggested alternatives about this.
We already come across a clear way to indicate that a function could potentially fail to return its result: using optionals. That is to say, return an optional<T>
, conveying the message that we expect to return a T
, but this could potentially fail, and the function caller is supposed to check whether that returned optional
is full or empty.
But here we’re talking about a function that returns nothing. It merely saves piece of data in a database. Should it return an optional<void>
then? This would read that it is supposed to return void
but it may return something that isn’t really a void
, but an empty box instead. An empty void. Weird. And std::optional<void>
doesn’t compile anyway!
Another possibility is to return a boolean indicating whether or not the function succeeded:
bool save(PieceOfData const& preciousData);
But this is less than ideal. First, the returned value could be ignored at call site. Though this could be prevented by adding the [[nodiscard]]
attribute in C++17:
[[nodiscard]] bool save(PieceOfData const& preciousData);
Second, just by looking at the function’s prototype, we don’t know if that bool
means success or failure. Or something else totally unrelated, for that matter. We could look it up in the documentation of the function, but it takes more time and introduces a risk of getting it wrong anyway.
Since the function is only called “save
“, its name doesn’t say what the return type represents. We could call it something like saveAndReturnsIfSuceeded
but… we don’t really want to see that sort of name in code, do we?
Meta information
It is interesting to realize that this is a more general use case that just failure or success. Indeed, sometimes the only way to retrieve a piece of information about a certain operation is to actually perform it.
For instance, say we have a function that takes an Input
and uses it to add and to remove entries from a existing Entries
collection:
void updateEntries(Input const& input, Entries& entries);
And we’d like to retrieve some data about this operation. Say an int
that represents the number of entries removed, for example. We could make the function output that int
via its return type:
int updateEntries(Input const& input, Entries& entries);
But the return type doesn’t tell what it represents here, only that it’s implemented as an int
. We’ve lost information here.
In this particular case, we could have added an int& entriesRemoved
function parameter, but I don’t like this pattern because it forces the caller to initialize a variable before calling the functions, which doesn’t work for all types, and a non-const reference means input-output and not output, so it’s not exactly the message we’d like to convey here.
What to do then?
Named return types: strong return types?
So in summary, we have return types that lack a meaningful name. This sounds like a job for strong types: indeed, strong types help put meaningful names over types!
Spoiler alert: strong types won’t be the option that we’ll retain for most cases of return types in the end. Read on to see why and what to use instead.
Let’s use NamedType
as an implementation of strong types, and create return types with a name that make sense in each of our functions’ contexts.
So our save
function returns a bool
that is true
if the operation was a success. Let’s stick a name over that bool
:
using HasSucceeded = NamedType<bool, struct HasSucceededTag>;
The second parameter of NamedType
is a “phantom type”, that is to say that it’s there only for differentiating HasSucceeded
from another NamedType
over a bool
.
Let’s use HasSucceeded
in our function’s interface:
HasSucceeded save(PieceOfData const& preciousData);
The function now expresses that it returns the information about whether the operation succeeded or not.
The implementation of the function would build a HasSucceeded
and return it:
HasSucceeded save(PieceOfData const& preciousData) { // attempt to save... // if it failed return HasSucceeded(false); // else, if all goes well return HasSucceeded(true); }
And at call site:
HasSucceeded hasSucceeded = save(myData); // or auto hasSucceeded = ... if(!hasSucceeded.get()) { // deal with failure...
Note that we can choose to get rid of the call to .get()
by making HasSucceeded
use the FunctionCallable
skill.
For the sake of the example, let’s apply the same technique to our updateEntries
function:
using NumberOfEntriesRemoved = NamedType<int, struct NumberOfEntriesRemovedTag>; NumberOfEntriesRemoved updateEntries(Input const& input, Entries& entries);
By looking at the interface, we now know that it outputs the number of entries removed via the return type.
Just a weak type will do here
The above works, but it’s needlessly sophisticated. In this case, the only thing we need is a name for other human beings to understand the interface. We don’t need to create a specific type used only in the context of the return type to also let the compiler know what we mean by it.
Why is that? Contrast our example with the case of input parameters of a function:
void setPosition(int row, int column); // Call site setPosition(36, 42);
Since there are several parameters that could be mixed up (and the program would still compile), introducing strong types such as Row
and Column
are useful to make sure we pass the parameters in the right order:
void setPosition(Row row, Column column); // Call site: setPosition(Row(36), Column(42));
But in the return type, what is there to mix up? There is only one value returned anyway!
So a simple alias does the job just well:
using HasSucceeded = bool; HasSucceeded save(PieceOfData const& preciousData);
This is the most adapted solution in this case, in my opinion.
The case where strong types are useful in return types
However there are at least two specific cases where strong types are helpful to clarify a returned value.
One is to use strong types to return multiple values.
The other is when you already have a strong type that represents the return value, and that you already use at other places in the codeline. For instance, if you have a strong type SerialNumber
that strengthen a std::string
, and you use it at various places, it makes perfect sense to return it from a function.
The point I want to make is not to create a strong type for the sole purpose of returning it from a function and immediately retrieving the value inside it afterwards. Indeed, in this case a classical alias will do.
What’s in a expressive function’s interface?
This technique helps us be more explicit about what it is that a function is returning.
This is part of a more general objective, which is to leverage on every element of the function to express useful information:
- a clear function name: by using good naming,
- well-designed function parameters (a 3-post series coming soon),
- an explicit output: either by returning the output directly (thus making functions functional), or by using an optional or, if it comes to that, returning something else, like we saw today. But always, by being the clearest possible about it.
You may also like:
- Strong types for strong interfaces
- Make your functions functional
- The right question for the right name
- How to choose good names in code
Share this post!