49

OK, so the title is a little clickbaity but seriously I've been on a tell, don't ask (TDA) 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, 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?

27
  • 3
    If you choose not to ignore Command Query Separation, would you consider your problem solved?
    – rwong
    Commented Feb 13, 2018 at 8:07
  • 37
    Consider that finding yourself on a kick might be an indication that you're engaging in dogma-driven design rather than reasoning out the pros and cons of each specific situation.
    – Blrfl
    Commented Feb 13, 2018 at 14:32
  • 5
    In general when people talk about "return" being harmful they're saying it's against structured programming, not functional, and a single return statement at the end of the routine (and maybe at the end of both sides of an if/else block that is itself the last element) is not included in that.
    – Random832
    Commented Feb 13, 2018 at 15:09
  • 19
    @jameslarge: False dichotomy. Not allowing yourself to be driven to designs by dogmatic thinking is not the same thing as the Wild West/everything goes approach you're talking about. The point is to not allow dogma to get in the way of good, simple, obvious code. Universal law is for lackies; context is for kings. Commented Feb 13, 2018 at 16:05
  • 6
    There's one thing for me that's unclear in your question: you say you've sworn off using 'return', but as far as I can tell you don't explicitly relate that to your other concepts or say why you've done this. When combined with the definition of a pure function including producing a result, you create something that is impossible to resolve.
    – Danikov
    Commented Feb 13, 2018 at 16:32

9 Answers 9

105

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.

25
  • 21
    @CandiedOrange: Whether a method have side effects directly or indirectly though many layers of calls does not change anything conceptually. It is still a side effect. But if the point is that side-effects only happens through injected objects so you control what kinds of side effects is possible, then this sounds like a good design. It is just not side-effect free. It is good OO but it is not purely functional.
    – JacquesB
    Commented Feb 13, 2018 at 12:40
  • 14
    @LightnessRacesinOrbit: grep in quiet mode has a process return code which will be used. If it didn't have that, then it would truly be useless
    – mike3996
    Commented Feb 14, 2018 at 13:13
  • 12
    @progo: A return code isn't a side effect; it's the effect. Anything we call a side effect is called a side effect because it isn't the return code ;) Commented Feb 14, 2018 at 13:15
  • 8
    @Cubic that's the point. A program without side effects is useless. Commented Feb 14, 2018 at 19:00
  • 5
    @mathreadler That's a side effect :)
    – Andres F.
    Commented Feb 14, 2018 at 20:58
40

Tell, Don't Ask comes with some fundamental assumptions:

  1. You're using objects.
  2. Your objects have state.
  3. The state of your objects affects their behavior.

None of these things apply to pure functions.

So let's review why we have the rule "Tell, Don't Ask." This rule is a warning and a reminder. It can be summarized like this:

Allow your class to manage its own state. Don't ask it for its state, and then take action based on that state. Tell the class what you want, and let it decide what to do based on its own state.

To put it another way, classes are solely responsible for maintaining their own state and acting on it. This is what encapsulation is all about.

From Fowler:

Tell-Don't-Ask is a principle that helps people remember that object-orientation is about bundling data with the functions that operate on that data. It reminds us that rather than asking an object for data and acting on that data, we should instead tell an object what to do. This encourages us to move behavior into an object to go with the data.

To reiterate, none of this has anything to do with pure functions, or even impure ones unless you're exposing a class's state to the outside world. Examples:

TDA Violation

var color = trafficLight.Color;
var elapsed = trafficLight.Elapsed;
If (color == Color.Red && elapsed > 2.Minutes)
    trafficLight.ChangeColor(green);

Not a TDA Violation

var result = trafficLight.ChangeColor(Color.Green);

or

var result = await trafficLight.ChangeColorWhenReady(Color.Green);     

In both of the latter examples, the traffic light retains control of its state and its actions.

17
  • 8
    @CandiedOrange closures are only pure if you don't modify the closed-over bindings. Otherwise you lose referential transparency when calling the function returned from the closure. Commented Feb 13, 2018 at 16:52
  • 2
    @JaredSmith and isn't the same true when you're talking about objects? Isn't this simply the immutability issue? Commented Feb 13, 2018 at 17:16
  • 2
    @bdsl - And now you've inadvertently pointed out exactly where every discussion of this type goes with your trafficLight.refreshDisplay example. If you follow the rules, you lead to a highly flexible system that nobody but the original coder understands. I'd even bet that after a couple years hiatus even the original coder probably won't understand what they did either.
    – Dunk
    Commented Feb 14, 2018 at 23:17
  • 2
    "Silly", as in "Do not open the other objects and look not at their guts but instead spill your own guts (if you have any) into the other objects methods; that's The Way"-silly.
    – Joker_vD
    Commented Feb 15, 2018 at 4:37
  • 2
    var result = trafficLight.ChangeColor(Color.Green); looks terrible and seems like the trafficLight will immediately change its color. Commented Sep 29, 2018 at 14:30
30

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.

You don't only ask for its internal state, you don't ask if it has an internal state at all either.

Also tell, don't ask! does not imply not getting a result in form of a return value (provided by a return statement inside the method). It just implies I don't care how you do it, but do that processing!. And sometimes you immediately want the processings result...

4
  • 1
    CQS though would imply that modifying state and getting results should be separated
    – jk.
    Commented Feb 13, 2018 at 11:23
  • 7
    @jk. As usual: in general you should separate state change and returning a result but in rare cases there are valid reasons to combine that. E.g.: an iterators next() method shouldn't only return the current object but also change the iterators internal state so that the next call returns the next object... Commented Feb 13, 2018 at 11:29
  • 4
    Exactly. I think OP’s problem is simply due to misunderstanding/misapplying “tell, don’t ask”. And fixing that misunderstanding makes the problem go away. Commented Feb 13, 2018 at 15:46
  • @KonradRudolph Plus, I don't think that's the only misunderstanding here. Their description of a "pure function" includes "Its state is set only once." Another comment indicates it could mean a closure's context, but the phrasing sounds odd to me.
    – Izkata
    Commented Feb 14, 2018 at 14:17
19

If you consider return as "harmful" (to stay in your picture), then instead of making a function like

ResultType f(InputType inputValue)
{
     // ...
     return result;
}

build it in a message-passing manner:

void f(InputType inputValue, Action<ResultType> g)
{
     // ...
     g(result);
}

As long as f and g are side-effect free, chaining them together will be side-effect free as well. I think this style is similar to what is also called Continuation-passing style.

If this really leads to "better" programs is debatable, since it breaks some conventions. The german software engineer Ralf Westphal made a whole programming model around this, he called it "Event Based Components" with a modeling technique he calls "Flow Design".

To see some examples, start in the "Translating to Events" section of this blog entry. For the full approach, there was once his e-book "Messaging as a Programming model - Doing OOP as if you meant it". Unfortunately, it seems to be hard to get these days.

21
  • 24
    If this really leads to "better" programs is debatable We only have to look at the code written in JavaScript during the first decade of this century. Jquery and its plugins were prone to this paradigm callbacks... callbacks everywhere. At a certain point, too many nested callbacks, made debugging a nightmare. Code still have to be read by humans regardless the eccentricities of the software engineering and its "principles"
    – Laiv
    Commented Feb 13, 2018 at 8:07
  • 1
    even then at some point you need to provide an action that performs a side effect or you need some way to return out of the CPS part
    – jk.
    Commented Feb 13, 2018 at 8:45
  • 12
    @Laiv CPS was invented as an optimization technology for compilers, nobody actually expected programmers to write the code this way by hand.
    – Joker_vD
    Commented Feb 13, 2018 at 12:37
  • 3
    @CandiedOrange In slogan form, "return is just telling the continuation what to do". Indeed, the creation of Scheme was motivated by trying to understand Hewitt's Actor model and came to the conclusion that actors and closures were the same thing. Commented Feb 13, 2018 at 13:43
  • 2
    Hypothetically, sure, you could write your entire application as a series of function calls that don't return anything. If you don't mind it being tightly coupled, you don't even need the ports. But most sane applications utilize functions that return things, because ... well, they're sane. And as I believe I've adequately demonstrated in my answer, you can return data from functions and still adhere to Tell Don't Ask, so long as you don't commandeer an object's state. Commented Feb 13, 2018 at 22:03
8

Message passing is inherently effectful. If you tell an object to do something, you expect it to have an effect on something. If the message handler was pure, you would not need to send it a message.

In distributed actor systems, the result of an operation is usually sent as a message back to the sender of the original request. The sender of the message is either implicitly made available by the actor runtime, or it is (by convention) explicitly passed as a part of the message. In synchronous message passing, a single response is akin to a return statement. In asynchronous message passing, using response messages is particularly useful as it allows for concurrent processing in multiple actors while still delivering results.

Passing the "sender" to which the result should be delivered explicitly basically models continuation passing style or the dreaded out parameters - except that it passes messages to them instead of mutating them directly.

5

This entire question strikes me as a 'level violation'.

You have (at least) the following levels in a major project:

  • The system level e.g. e-commerce platform
  • The sub-system level e.g. user validation: server, AD, front-end
  • The individual program level e.g. one of the components in the above
  • The Actor/Module level [this gets murky depending on language]
  • The method/function level.

And so on down to individual tokens.

There isn't really any need for an entity at the method/function level not to return (even if it just returns this). And there isn't (in your description) any need for an entity at the Actor level to return anything (depending on language that may not even be possible). I think the confusion is in conflating those two levels, and I would argue that they should be reasoned about distinctly (even if any given object actually spans multiple levels).

1

You mention that you want to conform to both the OOP principle of "tell, don't ask" and the functional principle of pure functions, but I don't quite see how that led you to eschew the return statement.

A relatively common alternative way of following both these principles is to go all-in on the return statements and use immutable objects with getters only. The approach then is that to have some of the getters return a similar object with a new state, as opposed to changing the state of the original object.

One example of this approach is in the Python builtin tuple and frozenset data types. Here's a typical usage of a frozenset:

small_digits = frozenset([0, 1, 2, 3, 4])
big_digits = frozenset([5, 6, 7, 8, 9])
all_digits = small_digits.union(big_digits)

print("small:", small_digits)
print("big:", big_digits)
print("all:", all_digits)

Which will print the following, demonstrating that the union method creates a new frozenset with its own state without affecting the old objects:

small: frozenset({0, 1, 2, 3, 4})

big: frozenset({5, 6, 7, 8, 9})

all: frozenset({0, 1, 2, 3, 4, 5, 6, 7, 8, 9})

Another extensive example of similar immutable data structures is Facebook's Immutable.js library. In both cases you start with these building blocks and can build higher-level domain objects that follow the same principles, achieving a functional OOP approach, which helps you encapsulate the data and reason about it more easily. And the immutability also lets you reap the benefit of being able to share such objects between threads without having to worry about locks.

1

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.

I've been trying my best to reconcile some of the benefits of, more specifically, imperative and functional programming (naturally not getting all the benefits whatsoever, but trying to get the lion's share of both), though return is actually fundamental to doing that in a straightforward fashion for me in many cases.

With respect to trying to avoid return statements outright, I tried to mull over this for the past hour or so and basically stack overflowed my brain a number of times. I can see the appeal of it in terms of enforcing the strongest level of encapsulation and information hiding in favor of very autonomous objects that are merely told what to do, and I do like exploring the extremities of ideas if only to try to get a better understanding of how they work.

If we use the traffic light example, then immediately a naive attempt would want to give such traffic light knowledge of the entire world that surrounds it, and that would certainly be undesirable from a coupling perspective. So if I understand correctly you abstract that away and decouple in favor of generalizing the concept of I/O ports which further propagate messages and requests, not data, through the pipeline, and basically inject these objects with the desired interactions/requests among each other while oblivious to each other.

The Nodal Pipeline

enter image description here

And that diagram is about as far as I got trying to sketch this out (and while simple, I had to keep changing it and rethinking it). Immediately I tend to think a design with this level of decoupling and abstraction would find its way becoming very difficult to reason about in code form, because the orchestrator(s) who wire all these things up for a complex world might find it very difficult to keep track of all these interactions and requests in order to create the desired pipeline. In visual form, however, it might be reasonably straightforward to just draw these things out as a graph and link everything up and see things happening interactively.

In terms of side effects, I could see this being free of "side effects" in the sense that these requests could, on the call stack, lead to a chain of commands for each thread to perform, e.g. (I don't count this as a "side effect" in a pragmatic sense as it is not altering any state relevant to the outside world until such commands are actually executed -- the practical goal to me in most software is not to eliminate side effects but defer and centralize them). And furthermore the command execution might output a new world as opposed to mutating the existing one. My brain is really taxed just trying to comprehend all this however, absent any attempt at prototyping these ideas. I also didn't try to tackle how to pass parameters along with the requests in favor of just trying a timid approach at first of thinking of all of these requests as nullary functions with a uniform signature/interface.

How it Works

So to clarify I was imagining how you actually program this. The way I was seeing it working was actually the diagram above capturing the user-end (programmer's) workflow. You can drag a traffic light into the world, drag a timer, give it an elapsed period (upon "constructing" it). The timer has an On Interval event (output port), you can connect that to the traffic light so that on such events, it's telling the light to cycle through its colors.

The traffic light might then, on switching to certain colors, emit outputs (events) like, On Red, at which point we might drag a pedestrian into our world and make that event tell the pedestrian to start walking... or we might drag birds into our scene and make it so when the light turns red, we tell birds to start flying and flapping their wings... or maybe when the light turns red, we tell a bomb to explode -- whatever we want, and with the objects being completely oblivious to each other, and doing nothing but indirectly telling each other what to do through this abstract input/output concept.

And they fully encapsulate their state and reveal nothing about it (unless these "events" are considered TMI, at which point I'd have to rethink things a lot), they tell each other things to do indirectly, they don't ask. And they're uber decoupled. Nothing knows about anything except this generalized input/output port abstraction.

Practical Use Cases?

I could see this type of thing being useful as a high-level domain-specific embedded language in certain domains to orchestrate all these autonomous objects which know nothing about the surrounding world, expose nothing of their internal state post construction, and basically just propagate requests among each other which we can change and tweak to our hearts' content. At the moment I feel like this is very domain-specific, or maybe I just haven't put enough thought into it, because it's very difficult for me to wrap my brain around with the types of things I regularly develop (I often work with rather low-mid-level code) if I were to interpret Tell, Don't Ask to such extremities and want the strongest level of encapsulation imaginable. But if we're working with high-level abstractions in a specific domain, this might be a very useful way to program it and express how things interact with each other in a rather uniform fashion that doesn't get muddled up in the state, or computations/outputs, of its objects, with uber decoupling of a kind where even the analogical caller need not not know much, if anything, about its callee, or vice versa.

Signals and Slots

This design looked oddly familiar to me until I realized it's basically signals and slots if we don't take a lot the nuances of how it's implemented into account. The main question to me is how effectively we can program these individual nodes (objects) in the graph as strictly adhering to Tell, Don't Ask, taken to the degree of avoiding return statements, and whether we can evaluate said graph without mutations (in parallel, e.g., absent locking). That's where the magical benefits are is not in how we wire these things together potentially, but how they can be implemented to this degree of encapsulation absent mutations. Both of these seem feasible to me, but I'm not sure how widely applicable it would be, and that's where I'm a bit stumped trying to work through potential use cases.

0
0

I clearly see leak of certainty here. It seems that "side-effect" is well-known and commonly-understood term, but in reality it's not. Depending upon your definitions (which are actually missing in the OP), side-effects might be totally necessary (as @JacquesB managed to explain), or mercylessly unaccepted. Or, making one step towards clarification, there is necessity to distinguish between desired side-effects one doesn't like to hide (at this points famous Haskell's IO emerges: it's nothing but a way to be explicit) and undesired side-effects as a result of code bugs and such kind of things. Those are pretty different problems and thus require different reasoning.

So, I suggest to start from rephrasing yourself: "How do we define side-effect and what does given definition(s) say about it's interrelation with "return" statement?".

1
  • 1
    "at this points famous Haskell's IO emerges: it's nothing but a way to be explicit" -- explicitness is certainly a benefit of Haskell's monadic IO, but there is another point to it: it provides a means of isolating the side effect to be entirely outside of the language -- although common implementations don't actually do this, mostly due to efficiency concerns, conceptually it is true: the IO monad can be considered to be a way of returning an instruction to some environment that is entirely outside of the Haskell program, along with a reference to a function to continue after completion.
    – Jules
    Commented Feb 14, 2018 at 19:33

Not the answer you're looking for? Browse other questions tagged or ask your own question.