Functional Programming – Can Code Be Functional Without ‘Return’?

Architecturefunctional programmingobject-orientedside-effect

OK, so the title is a little clickbaity but seriously I've been on a tell, don't ask kick for a while. I like how it encourages methods to be used as messages in true object-oriented fashion. But this has a nagging problem that has been rattling about in my head.

I have come to suspect that well-written code can follow OO principles and functional principles at the same time. I'm trying to reconcile these ideas and the big sticking point that I've landed on is return.

A pure function has two qualities:

  1. Calling it repeatedly with the same inputs always gives the same result. This implies that it is immutable. Its state is set only once.

  2. It produces no side effects. The only change caused by calling it is producing the result.

So, how does one go about being purely functional if you've sworn off using return as your way of communicating results?

The tell, don't ask idea works by using what some would consider a side effect. When I deal with an object I don't ask it about its internal state. I tell it what I need to be done and it uses its internal state to figure out what to do with what I've told it to do. Once I tell it, I don't ask what it did. I just expect it to have done something about what it was told to do.

I think of Tell, Don't Ask as more than just a different name for encapsulation. When I use return I have no idea what called me. I can't speak it's protocol, I have to force it to deal with my protocol. Which in many cases gets expressed as the internal state. Even if what is exposed isn't exactly state it's usually just some calculation performed on state and input args. Having an interface to respond through affords the chance to massage the results into something more meaningful than internal state or calculations. That is message passing. See this example.

Way back in the day, when disk drives actually had disks in them and a thumb drive was what you did in the car when the wheel was too cold to touch with your fingers, I was taught how annoying people consider functions that have out parameters. void swap(int *first, int *second) seemed so handy but we were encouraged to write functions that returned the results. So I took this to heart on faith and started following it.

But now I see people building architectures where objects let how they were constructed control where they send their results. Here's an example implementation. Injecting the output port object seems a bit like the out parameter idea all over again. But that's how tell-don't-ask objects tell other objects what they've done.

When I first learned about side effects I thought of it like the output parameter. We were being told not to surprise people by having some of the work happen in a surprising way, that is, by not following the return result convention. Now sure, I know there's a pile of parallel asynchronous threading issues that side effects muck about with but return is really just a convention that has you leave the result pushed on the stack so whatever called you can pop it off later. That's all it really is.

What I'm really trying to ask:

Is return the only way to avoid all that side effect misery and get thread safety without locks, etc. Or can I follow tell, don't ask in a purely functional way?

Best Answer

If a function doesn't have any side effects and it doesn't return anything, then the function is useless. It is as simple as that.

But I guess you can use some cheats if you want to follow the letter of the rules and ignore the underlying reasoning. For example using an out parameter is strictly speaking not using a return. But it still does precisely the same as a return, just in a more convoluted way. So if you believe return is bad for a reason, then using an out parameter is clearly bad for the same underlying reasons.

You can use more convoluted cheats. E.g. Haskell is famous for the IO monad trick where you can have side effects in practice, but still not strictly speaking have side effects from a theoretical viewpoint. Continuation-passing style is another trick, which well let you avoid returns at the price of turning your code into spaghetti.

The bottom line is, absent silly tricks, the two principles of side-effect free functions and "no returns" are simply not compatible. Furthermore I will point out both of them are really bad principles (dogmas really) in the first place, but that is a different discussion.

Rules like "tell, don't ask" or "no side effects" cannot be applied universally. You always have to consider the context. A program with no side effects is literally useless. Even pure functional languages acknowledge that. Rather they strive to separate the pure parts of the code from the ones with side-effects. The point of the State or IO monads in Haskell is not that you avoid side effects - because you can't - but that the presence of side effects is explicitly indicated by the function signature.

The tell-dont-ask rule applies to a different kind of architecture - the style where objects in the program are independent "actors" communicating with each other. Each actor is basically autonomous and encapsulated. You can send it a message and it decides how to react to it, but you cannot examine the internal state of the actor from the outside. This means you cannot tell if a message changes the internal state of the actor/object. State and side effects are hidden by design.