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?

candied_orange
  • 102,279
  • 24
  • 197
  • 315
  • 3
    If you choose not to ignore Command Query Separation, would you consider your problem solved? – rwong Feb 13 '18 at 08:07
  • @rwong I can see how to follow CQS under OO just fine. It's following it under the requirements of pure functional programming that has me scratching my head. If you can see it please explain. – candied_orange Feb 13 '18 at 08:13
  • 1
    I don't think you should call a function immutable. What you should be referring to is that a pure function doesn't rely on things like global variables. – Pieter B Feb 13 '18 at 10:30
  • 2
    @PieterB strictly speaking I'm talking about methods or closures (same dif for this issue) that have access to state beyond their input parameters. I'm merely pointing out the need for that state to be immutable to fulfil the first requirement of pure functional programing that I mentioned. Being globally accessible or not is an encapsulation issue but not relevant here. – candied_orange Feb 13 '18 at 10:55
  • 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 Feb 13 '18 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 Feb 13 '18 at 15:09
  • As for "state beyond their input parameters", generally "this" or closure state is either considered an input parameter or part of the function definition (the function is a distinct function from other functions with other "this"es, even if they have the same body). – Random832 Feb 13 '18 at 15:14
  • @Random832 Yes — essentially, `this` is the function's closure. – Theodoros Chatzigiannakis Feb 13 '18 at 15:55
  • 1
    @Blrfl, which would you rather try to read and understand? A system where every method, every class, every module and every interface is designed according to the same guiding principles? or a system where each of the above was designed by a different person with a different idea about how the parts should fit together? There can be benefits to limiting yourself to one particular style/paradigm/philosophy/whatever-you-may-call-it. (And, you can triple that, or maybe quadruple it, if the language is C++.) – Solomon Slow Feb 13 '18 at 15:59
  • 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. – Nicol Bolas Feb 13 '18 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 Feb 13 '18 at 16:32
  • 1
    @Danikov I've constructed working flexible programs following TDA and avoiding use of return in an exploration of how much I can do working this way. Where before I had always reached for return thinking it was the only way. [Here's an example](https://codereview.stackexchange.com/questions/148809/a-button-as-a-clean-architecture-plugin) I created without even thinking about TDA or functional purity. I look at it now though and it seems to follow TDA very well. I'm trying to look at it while thinking about functional purity. Is it crazy to think it could be described as pure? Even parts of it? – candied_orange Feb 13 '18 at 17:01
  • @NicolBolas, I never meant to suggest that "dogma-driven design" and "The Wild West" were the only two possibilities. In fact, I was trying to lead to exactly the opposite conclusion. Blrfl thinks that dogma-driven is bad. I agree. You think that the Wild West is bad. I agree. IMO, experimenting with a style (e.g., going off on a "tell-don't-ask kick") is a better choice than either of the above. – Solomon Slow Feb 13 '18 at 17:22
  • 3
    @jameslarge Experimentation with something to see if it's going to produce good results in a given situation is reasoning. Predisposition to use it is going off on a kick. – Blrfl Feb 13 '18 at 17:33
  • @Blrfl my personal inclination to be predisposed is exactly what I'm trying to check myself against. I've come come to like this and I want to be sure my reasons why aren't simply fantasies. – candied_orange Feb 13 '18 at 18:23
  • @CandiedOrange That is pretty much how TDD came about. Beck had a intuition that testing first, like in other engineering disciplines could work. So he went on a "kick", and tried it out. However, you need to convince others to join your trip down the rabbit hole, if you want to battle your own predisposition. I might suggest not using reason to achieve this, but instead say "I don't know why this works, want to help me figure it out?" But I must say, excellent thread, threads like this is what I joined this community to be a part of. – Chris Wohlert Feb 14 '18 at 09:39
  • @ChrisWohlert Thanks, and please help me figure it out. Wish I knew what needs clarifying to remove the hold. – candied_orange Feb 14 '18 at 11:28
  • 2
    "X considered harmful" statements 101: find out the implied context. Consider X to be totally fine except in that particular context. – guillaume31 Feb 14 '18 at 12:51
  • 3
    "public boolean isPushed() { return pushed; }" I see a return in that example. You still haven't justified your reason for avoiding return. What's the motivation, beyond the vague title statement that return is "considered harmful"? 'Considered harmful' and 'should never be used' are very different things. If it's just a flying fancy to see if it can be done, then it wouldn't hurt to state that explicitly, but it'd be nice to understand the thinking behind how you got to that idea. – Danikov Feb 14 '18 at 15:44
  • @Danikov It came from a few sources. Tell don't ask leaves me inclined to not ask questions. Nor provide a way to ask questions. But instead provide ways for objects to tell each other things. Clean Architecture tells me to use plugins with input and output port interfaces. These don't use return. What I've discovered is that I can solve just about any problem without using `return`. Which surprised me. So now that I have this strange power, I'm trying to figure out how to use it wisely. I'm looking for downsides. Purity? You're right about `isPushed()`. I could avoid that too. Should I? – candied_orange Feb 14 '18 at 21:27
  • @Danikov strangely enough, another reason comes from "favor composition over inheritance". One of the nice things about composition is you use what you're composed of from the outside not the inside. You use it through it's normal interface. That's also true when I use an output port. It's not true when I use return or throw exceptions at my client. – candied_orange Feb 14 '18 at 21:33
  • Functional and OO are different approaches and solve slightly different problems. OO is mostly focused in code organization and data encapsulation and enables a designer to get a good overview of their code more quickly... Functional is an amazing way to eliminate concurrency problems and is a good way to break down complex problems. They can be used in combination but going to an extreme with either is probably harmful (Like, say, trying to forcefully combine "tell don't ask" with immutability...) – Bill K Feb 14 '18 at 21:42
  • @BillK hey I'm not trying to force it. I'm wondering if it's even possible. That's why I'm asking if getting the benefits functional purity is possible without using return. For that, is return the only way? – candied_orange Feb 14 '18 at 21:56
  • @CandiedOrange I don't think it's possible--In a way "Tell Don't ask" is the opposite of Immutable. Another way to put it: A function MUST have a return value or it's not a function. However remember in real life--you can integrate the two styles very successfully by coding most of your methods as functions and most of your objects as immutable, avoiding most getters and pretty much all setters.... You can combine the two styles pretty well. It still leans towards OO more than functional. – Bill K Feb 15 '18 at 00:04
  • It's possible that the programming paradigm you're looking for is something like https://www.destroyallsoftware.com/talks/boundaries – FacticiusVir Feb 15 '18 at 10:54
  • `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.` Not necessarily. A clear and simple counterexample of this is JSONP. JSONP only really asks for the callback method name, but you can expand this to include a full on formatter with specific implementations for every needed protocol. If the protocol is included as a parameter, then the method itself doesn't need to care what the exact protocol is. – Flater Feb 15 '18 at 12:29
  • 1
    sounds like you've discovered an O-O "workaround" to provide continuations - i.e., "continuation passing style" - something that is totally natural in functional languages (where you use closures for that, with a nice clean syntax) yet is _still_ considered unwieldy to use _exclusively_ instead of functions returning results - suitable only for special purposes like compilers or other situations where you absolutely must have explicit control flow - oh wait, explicit control flow? I thought that's what you were trying to _get away from_? – davidbak Dec 16 '21 at 23:07

9 Answers9

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.

JacquesB
  • 57,310
  • 21
  • 127
  • 176
  • So purely function code without a return isn't possible without convoluted cheats and hard to read tricks. Is it convoluted and/or not purely functional if during construction I inject into an object, another object that will receive the results of all the method calls? Consider [this example implemntation of Clean Architecture](https://codereview.stackexchange.com/q/148809/40367). Particularly note the `testEndToEnd` method that traverses the entire object graph. Is this functional? Is it too convoluted and tricky? – candied_orange Feb 13 '18 at 12:13
  • @CandiedOrange: Depends - does the object which you inject use returns or have side effects? – JacquesB Feb 13 '18 at 12:17
  • Not until the last one that flow of control touches, which is needed to finally give in and, as you say, make the program useful. The difference where that side effect ends up. Here I'm making the side effect `ButtonPresenter` the first thing composed not the last thing. But I am making that the only place for side effects. Can that be considered close enough to normal functional code to provide similar benefits or is it inherently flawed? – candied_orange Feb 13 '18 at 12: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 Feb 13 '18 at 12:40
  • I believe the idea is to push the side-effects into one area rather than let them roam free. But doing them inside out like this means whenever you use them, or even test them at their hart will be exactly one side effect that lets you know what happened. I think this might sour the idea of optimizing by caching what a functions result will be. But I'm not sure what other benefits have been lost. – candied_orange Feb 13 '18 at 12:46
  • 2
    _"A program with no side effects is literally useless"_ Really? Isn't that what e.g. `grep` does (in quiet mode)? I'd contend that the basis of all programs is those with no side effects and only a [useful] return code. Ofc more complex programs with more complex semantics then introduce things like console output to give us more to work with. – Lightness Races in Orbit Feb 13 '18 at 15:44
  • Otherwise nice answer – Lightness Races in Orbit Feb 13 '18 at 15:45
  • @LightnessRacesinOrbit: Good point, although Grep reads from the file system, so it is not pure in a functional sense, as the original question defines it. – JacquesB Feb 13 '18 at 16:04
  • 1
    @JacquesB grep doesn't need to read from the file system though. In a pipeline, it's basically just a pure function `String -> String` (with lazy strings). – leftaroundabout Feb 14 '18 at 13:02
  • 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 Feb 14 '18 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 ;) – Lightness Races in Orbit Feb 14 '18 at 13:15
  • 1
    @LightnessRacesinOrbit: oh yes, right you are. How very sleepy of me – mike3996 Feb 14 '18 at 13:37
  • 1
    @progo: I'll let you off because it's still only February (ugh) – Lightness Races in Orbit Feb 14 '18 at 13:49
  • 3
    @LightnessRacesinOrbit grep in silent mode as a program, i.e. on its own is useless. Only if you use the result in some way, to eventually cause a side effect (print to console, delete stuff found ...), then grep has value. In that sense, a silent grep is just a function. Of course, the definition program/function needs some work to make this waterproof. – Jens Schauder Feb 14 '18 at 14:47
  • 1
    @JensSchauder I don't see how that's relevant. Any program is useless in isolation. A web server is useless if no clients ever connect to it. A general AI is useless if it doesn't get any inputs or its outputs aren't consumed by anything. – Cubic Feb 14 '18 at 15:21
  • 8
    @Cubic that's the point. A program without side effects is useless. – Jens Schauder Feb 14 '18 at 19:00
  • @JensSchauder not necessarily, it could do synchronization of time to prepare for the program after it. – mathreadler Feb 14 '18 at 20:58
  • 5
    @mathreadler That's a side effect :) – Andres F. Feb 14 '18 at 20:58
  • 3
    You must remember that changing pixels on the screen and affecting the user's neural state are side effects, and therefore looked down upon in pure functional circles. – Asad Saeeduddin Feb 14 '18 at 21:51
  • @AndresF. okay, I thought some data in the program needed to change somehow as consequence by the code for something to be considered a side effect. :) But maybe we can see it as a side effect on system level. – mathreadler Feb 15 '18 at 05:28
  • 2
    @leftaroundabout: Some have indeed argued that the unix pipeline is functional composition. But in any case, at some point you need to provide the input and you need to do some side effect with the output (display it store it, use as input to another process) for it to be useful. – JacquesB Feb 15 '18 at 10:50
  • 1
    @CandiedOrange "I believe the idea is to push the side-effects into one area..." Yes, that's a pragmatic way of looking at it (hence monads), and tell-dont-ask (like all rules-of-thumb) should also be applied pragmatically. Tell-dont-ask is aimed at a specific anti-pattern: pulling data from an object and performing an operation on that data (and even updating the object with the results), when the operation is best seen as a responsibility of the object. Quite often, the 'teller' has a legitimate interest in the result of the operation, and 'return' is often a pragmatic way to convey that. – sdenham Feb 15 '18 at 14:19
  • @sdenham you're right of course. But I'm exploring the possibilities of removing the tellers interest in the results. To push that concern down into what I'm talking to. It's weird and different but very liberating. The idea that I can do all my communicating facing the normal outside Interface appeals to me. That makes the abstractions very powerful because you're always facing them the right way. But I'm still exploring the downsides. Making this practical may be a fantasy. – candied_orange Feb 15 '18 at 14:57
  • That will be at least an interesting exercise, but you might want to be careful of doing the opposite: moving responsibilities of the teller into the actor simply to avoid having the teller ask anything. – sdenham Feb 15 '18 at 17:30
  • @sdenham well really I'm pushing the responsibility to deal with the result not just into the actor but out the other side to another object, that was set when the actor was constructed, and talking to it through an output interface. That lets the result out through a polymorphicly deployed message. – candied_orange Feb 17 '18 at 21:25
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.

Robert Harvey
  • 198,589
  • 55
  • 464
  • 673
  • 1
    Wait a second, closures can be pure. they have state (they call it lexical scope). and that lexical scope can affect their behavior. Are you sure TDA is only relevant when objects are involved? – candied_orange Feb 13 '18 at 16:43
  • 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. – Jared Smith Feb 13 '18 at 16:52
  • 2
    @JaredSmith and isn't the same true when you're talking about objects? Isn't this simply the immutability issue? – candied_orange Feb 13 '18 at 17:16
  • @CandiedOrange: Open [this page](http://gorodinski.com/blog/2013/09/18/oop-patterns-from-a-functional-perspective/) in chrome, and search it for the words Tell, Don’t Ask (including the punctuation). – Robert Harvey Feb 13 '18 at 17:21
  • @CandiedOrange I think we're using different meaning of 'state'. If by state you mean partially applied parameters captured in a closure, then yeah sure you can have closures that are pure. But when I hear state I hear 'mutable state' in which case no. So yes to the question, immutability whether we're talking about objects or closures. The link in the comment above has a good characterization of what TDA looks like in functional code (specifically encoding it into the type system and dispatching based on pattern-matching). – Jared Smith Feb 13 '18 at 17:37
  • @JaredSmith You're right we are. When I say I mean state. My birthdate and my mood are both state. One is more likely to change than the other. – candied_orange Feb 14 '18 at 07:59
  • @RobertHarvey In my head TDA is not just a different name for encapsulation. TDA asks a little more. When you return that the traffic light is green you have exposed state. But if you never expose anything the only result of running a program is a blinking cursor. So we have to let something out. TDA gives a way of exposing state that is more controlled. Not just anyone can ask the color of the traffic light. Under TDA I could tell the light a car is awaiting clearance to proceed. The light could respond with car.cleared() and we wouldn't know or care if the light is green or blinking yellow. – candied_orange Feb 14 '18 at 08:07
  • @RobertHarvey in this way the information exposed is controlled by funneling it into an understood context rather then rolling over and offering it to everyone. When I know what context I'm talking to I can make what I say focused on being meaningful in that context and hide the parts of it that are not relevant. – candied_orange Feb 14 '18 at 08:11
  • @RobertHarvey is that convenient for the traffic light? No. A simple return lets the traffic light ignore the car's needs and language it want to talk in. But by taking the time to understand what the car cares about the traffic light is free to keep any knowledge of its state and how it represents its state out of the car's hands. Sure it adds boilerplate code but are you sure this approach is always wrong? If it's not, can it be understood in a functionally pure way? I used to think of `return` as the only way to get info out in a non side effect way. Was that right? Was it over simplified? – candied_orange Feb 14 '18 at 08:33
  • @CandiedOrange: I'm saying in my answer that you can return something from a method without violating TDA. The traffic light example merely illustrates this possibility, nothing more. – Robert Harvey Feb 14 '18 at 16:11
  • Would a better 'not a TDA' example be one where the traffic light code is refactored so that the if condition moves inside the TrafficLight class? And the calling code just becomes trafficLight.refreshDisplay() – bdsl Feb 14 '18 at 20:40
  • @bdsl: Possibly, but since all I'm illustrating is that you can return a value without violating TDA, I didn't include that level of detail in my example. – Robert Harvey Feb 14 '18 at 20:57
  • 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 Feb 14 '18 at 23:17
  • How do I separate the traffic light from the colour-change timer withuot breaking TDA? I can give the traffic light method `ChangeColorWhenReady(Color newColor, ReadinessDecider decider);` where `ReadinessDecider` is a one-method interface with `bool DecideIfReady(Color color, Time elapsedTime)`, but that seems sort of silly. – Joker_vD Feb 15 '18 at 04:31
  • 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 Feb 15 '18 at 04:37
  • @Joker_vD: TDA does not require that you abstract behavior into a separate class and inject it. Quite the opposite, in fact. – Robert Harvey Feb 15 '18 at 05:36
  • 1
    `var result = trafficLight.ChangeColor(Color.Green);` looks terrible and seems like the trafficLight will immediately change its color. – Koray Tugay Sep 29 '18 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...

Timothy Truckle
  • 2,336
  • 9
  • 12
  • 1
    CQS though would imply that modifying state and getting results should be separated – jk. Feb 13 '18 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... – Timothy Truckle Feb 13 '18 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. – Konrad Rudolph Feb 13 '18 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 Feb 14 '18 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.

Doc Brown
  • 199,015
  • 33
  • 367
  • 565
  • 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](https://en.wikipedia.org/wiki/Callback_(computer_programming)), made debugging a nightmare. Code still have to be read by humans regardless the eccentricities of the software engineering and its "principles" – Laiv Feb 13 '18 at 08: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. Feb 13 '18 at 08:45
  • @DocBrown so purely functional code without a return is possible. Would this still work if `f` were a method on `x` and `g` had been passed to `x` when it was constructed? Meaning I could call `x.f(inputValue)` and expect `g(result)` to be called. – candied_orange Feb 13 '18 at 11:07
  • 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 Feb 13 '18 at 12:37
  • 1
    @CandiedOrange is not that a simple `Handler`? `OnFSuccessed()`/`OnFailed()`. – Laiv Feb 13 '18 at 13:13
  • 3
    @CandiedOrange In slogan form, "return is just telling the continuation what to do". Indeed, the creation of Scheme [was motivated by](https://www.dreamsongs.com/Files/HOPL2-Uncut.pdf#page=33) trying to understand Hewitt's Actor model and came to the conclusion that actors and closures were the same thing. – Derek Elkins left SE Feb 13 '18 at 13:43
  • @CandiedOrange: is this a rhetorical question? Sure one can inject a function or "Action" or "Handler" g into another object x and call it from every method of x. We do this all the time, for example, when wiring event handlers in GUIs together. I am sure I don't have to explain you this, you are not a beginner. – Doc Brown Feb 13 '18 at 19:00
  • @DocBrown where I'm going with that is to ask if such code could be considered pure, functional, thread safe, all that jazz. Can I have that if I don't use return? Or does pure functional programming require use of return? – candied_orange Feb 13 '18 at 19:20
  • @CandiedOrange: well, I guess as long as x is an immutable object, this still counts as pure. – Doc Brown Feb 13 '18 at 19:25
  • @DocBrown cool. Does it still count if, at the end of a chain of immutable objects, you find an object that talks to the console? FP uses fancy monads for this. Is there something fancy I need for this to be "right" or is this fine? – candied_orange Feb 13 '18 at 19:31
  • @CandiedOrange: as soon as there is one function in the chain which has side effects or changes global state, the whole program does. But then we encounter the situation scetched by JaquesB: when a *program* has no return value and zero side effects, it is useless. Any useful program will somewhere take some inputs, and make some outputs. "pure functional" is something which can happen between these two ends, but that's it. – Doc Brown Feb 13 '18 at 19:39
  • @DocBrown that is exactly what I was hoping to hear. Sounds like return is not required. Instead I can use what Uncle Bob calls "output ports". – candied_orange Feb 13 '18 at 19:43
  • @CandiedOrange: Fundamentally, an "Output Port" is just an `Interface`. If you expect to output something from that interface, you're still going to have to return something from a method implementation. – Robert Harvey Feb 13 '18 at 20:06
  • 1
    @RobertHarvey I can send results by making a call to whatever is implementing that interface rather than returning to what called me. – candied_orange Feb 13 '18 at 20:22
  • @CandiedOrange: Is that all this is about? Let's say you want to get some result based on an initial "Tell." Wouldn't you have to hook an event to your output port to get that information? How would you correlate it with the initial Tell? Wouldn't you have to also output an ID from your original Tell? Seems like an awful lot of trouble if all you need is a boolean confirmation or a DTO. Like many other things in software development, TDA has "suitable" uses. You don't use a jackhammer when a ball-peen hammer will suffice. – Robert Harvey Feb 13 '18 at 20:55
  • @RobertHarvey well yes, this or a hook, event, lambda, or callback would all let me polymorphically deploy my result without me or what called me knowing where my result is going. That's what I get by not using return. What I'm asking about is what that costs me. Am I losing the ability to be purely functional? At least as pure as anything that works. So far no ones made a case that I am. – candied_orange Feb 13 '18 at 21:56
  • 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. – Robert Harvey Feb 13 '18 at 22:03
  • @RobertHarvey Goto used to be considered sane but I'm not arguing that you must code this way. Want to figure out if doing so cripples the ability to keep pure. If I see people coding this way, can I argue that they're spreading harmful side effects around or is this style just making spotting such problems more complicated? I've seen claims that clean architecture is a functional way to program. I'm wondering if my inability to see that is because in my head sending a result out in anything other than a return is a potentially harmful side effect. Is that way of analysing is to limited? – candied_orange Feb 14 '18 at 07:40
  • @CandiedOrange: The very idea that you could write a functional program without `return` statements is... inconvenient at best. All functional languages depend on such returns. The cost of such a programming style almost certainly exceeds the benefits. – Robert Harvey Feb 14 '18 at 16:15
  • @CandiedOrange: I have added a link to Westphal's e-book in my answer. Apparently it is now freely available (payment only by choice). There you will probably find the things you are looking for. – Doc Brown Feb 14 '18 at 16:50
  • @Laiv: What has changed in the current century? async/await is quite new and still not supported by IE... – Heinzi Feb 15 '18 at 12:38
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.

Bergi
  • 996
  • 7
  • 15
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).

Jared Smith
  • 1,620
  • 12
  • 18
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.

yoniLavi
  • 351
  • 4
  • 8
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

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?".

Zazaeil
  • 345
  • 1
  • 8
  • 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 Feb 14 '18 at 19:33