17

"The ideal number of arguments for a function is zero" is plain wrong. The ideal number of arguments is exactly the number needed to enable your function to be side-effect free. Less than that and you needlessly cause your functions to be impure thus forcing you to steer away from the pit of success and climb the gradient of pain. Sometimes "Uncle Bob" is spot on with his advice. Sometimes he is spectacularly wrong. His zero arguments advice is an example of the latter

(Source: comment by @David Arno under another question on this site)

The comment has gained a spectacular amount of 133 upvotes, which is why I'd like to pay some closer attention to its merit.

As far as I'm aware, there are two separate ways in programming: pure functional programming (what this comment is encouraging) and tell, don't ask (which from time to time is being recommended on this website as well). AFAIK these two principles are fundamentally incompatible, close to being opposites of each other: pure functional can be summarized as "only return values, have no side effects" while tell, don't ask can be summarized as "don't return anything, only have side effects". Also, I'm kind of perplexed because I thought that tell, don't ask was considered the core of OO paradigm while pure funcitons were considered the core of functional paradigm - now I see pure functions recommended in OO!

I suppose developers should likely chose one of these paradigms and stick to it? Well I must admit I could never bring myself to follow either. Oftentimes it seems convenient for me to return a value and I can't really see how can I achieve what I want to achieve only with side effects. Oftentimes it seems convenient for me to have side effects and I can't really see how can I achieve what I want to achieve only by returning values. Also, oftentimes (I guess this is horrible) I have methods that do both.

However, from these 133 upvotes I'm reasoning that currently pure functional programming is "winning" as it becomes a consensus that it is superior to tell, don't ask. Is this correct?

Therefore, on the example of this antipattern-ridden game I'm trying to make: If I wanted to bring it to conformance with pure functional paradigm - HOW?!

It seems reasonable to me to have a battle state. Since this is a turn based game, I keep battle states in a dictionary (multiplayer - there may be many battles played by many players at the same time). Whenever a player makes their turn, I call an appropriate method on the battle state which (a) modifies the state accordingly and (b) returns updates to the players, which get serialized into JSON and basically just tell them what has just happened on the board. This, I suppose, is in a flagrant violation of BOTH principles and at the same time.

OK - I could make a method RETURN a battle state instead of modifying it in place if I really wanted to. But! Will I then have to copy everything in the battle state needlessly just to return a wholly new state instead of modifying it in place?

Now perhaps if the move is an attack I could just return a characters updated HP? Problem is, it's not that simple: game rules, a move can and often will have many more effects than just removing a portion of a player's HP. For example, it may increase the distance between characters, apply special effects, etc etc.

It seems so much simpler for me to just modify the state in place and return updates...

But how would an experienced engineer tackle this?

2
  • 9
    Following any paradigm is a sure way to failure. Policy should never trump intelligence. The solution to a problem should depend upon the problem, not your religious beliefs about problem solving.
    – John Douma
    Mar 6, 2019 at 20:58
  • 2
    I've never had a question asked here about something I've said before. I'm honoured. :)
    – David Arno
    Mar 7, 2019 at 13:44

6 Answers 6

17

Like most programming aphorisms, "tell, don't ask" sacrifices clarity to gain brevity. It is not at all intended to recommend against asking for the results of a calculation, it is recommending against asking for the inputs of a calculation. "Don't get, then calculate, then set, but it's okay to return a value from a calculation," isn't as pithy.

It used to be fairly common for people to call a getter, do some calculation on it, then call a setter with the result. This is a clear sign your calculation actually belongs to the class you called the getter on. "Tell, don't ask" was coined to remind people to be on the lookout for that anti-pattern, and it worked so well that now some people think that part is obvious, and they look for other kinds of "asks" to eliminate. However, the aphorism is only usefully applied to that one situation.

Pure functional programs never suffered from that exact anti-pattern, for the simple reason that there are no setters in that style. However, the more general (and more difficult to see) problem of not mixing different semantic abstraction levels in the same function applies to every paradigm.

1
  • 1
    Thank you for correctly explaining "Tell, don't Ask".
    – user949300
    Mar 7, 2019 at 17:27
16

Both Uncle Bob and David Arno (the author of the quote you had) have important lessons we can glean from what they wrote. I think it's worth learning the lesson and then extrapolating what that really means for you and your project.

First: Uncle Bob's Lesson

Uncle Bob is making the point that the more arguments you have in your function/method the more developers who use it have to understand. That cognitive load doesn't come for free, and if you aren't consistent with the order of the arguments, etc. the cognitive load only increases.

That is a fact of being human. I think the key mistake in Uncle Bob's Clean Code book is the statement "The ideal number of arguments for a function is zero". Minimalism is great until it isn't. Just like you never reach your limits in Calculus, you'll never reach "ideal" code--nor should you.

As Albert Einstein said, “Everything should be as simple as it can be, but not simpler”.

Second: David Arno's Lesson

The way of developing David Arno described is more functional style development than object oriented. However, functional code scales way better than traditional object oriented programming. Why? Because of locking. Any time state is mutable in an object, you run the risk of race conditions or locking contention.

Having written highly concurrent systems used in simulations and other server side applications, the functional model works wonders. I can attest to the improvements the approach has made. However, it is a very different style of development, with different requirements and idioms.

Development is a series of trade-offs

You know your application better than any of us do. You may not need the scalability that comes with functional style programming. There is a world between the two ideals listed above. Those of us who deal with systems that need to handle high throughput and ridiculous parallelism will tend toward the ideal of functional programming.

That said, you can use data objects to hold the set of information you need to pass in to a method. That helps with the cognitive load problem that Uncle Bob was addressing, while still supporting the functional ideal that David Arno was addressing.

I've worked on both desktop systems with limited parallelism required, and high throughput simulation software. They have very different needs. I can appreciate well written object oriented code that is designed around the concept of data hiding you are familiar with. It works for several applications. However, it doesn't work for all of them.

Who's right? Well, David is more right than Uncle Bob in this case. However, the underlying point I want to underscore here is that a method should have as many arguments as make sense.

2
  • There is paralelism. Different battles can be processed in parallel. However, yes: a single battle, while it is being processed, needs to be locked.
    – gaazkam
    Mar 7, 2019 at 11:57
  • Yes, I meant that the readers (reapers in your analogy) would glean from their (the sower) writings. That said, I have gone back to look at some things I've written in the past and have either relearned something or disagreed with my former self. We are all learning and evolving, and that is reason number one why you should always reason through how and if you apply something you learned. Mar 7, 2019 at 13:18
9

As the author of the comment, I guess I should clarify it here, as of course there's more to it than the simplified version that my comment offers.

AFAIK these two principles are fundamentally incompatible, close to being opposites of each other: pure functional can be summarized as "only return values, have no side effects" while tell, don't ask can be summarized as "don't return anything, only have side effects".

To be honest, I find this a really strange use of the term "tell, don't ask". So I had a read of what Martin Fowler said on the subject a few years ago, which was enlightening. The reason I found it strange is because "tell don't ask" is synonymous with dependency injection in my head and the purest form of dependency injection is passing everything a function needs in via its parameters.

But it appears that the meaning I apply to "tell don't ask" comes from taking Fowler's OO-focused definition and making it more paradigm agnostic. In the process, I believe it takes the concept to its logical conclusions.

Let's wind back to simple beginnings. We have "lumps of logic" (procedures) and we have global data. Procedures read that data directly in order to access it. We have an simple "ask" scenario.

Wind forward a bit. We now have objects and methods. That data no longer needs to be global, it can be passed in via the constructor and contained within the object. And then we have methods that act upon that data. So now we have "tell, don't ask" as Fowler describes it. The object is told its data. Those methods no longer have to ask the global scope for their data. But here's the rub: this still isn't true "tell, don't ask" in my view as those methods still have to ask the object scope. This is more a "tell, then ask" scenario I feel.

So wind forward to the modern day, dump the "it's OO all the way down" approach and borrow some principles from functional programming. Now when a method is called, all the data is supplied to it via its parameters. It can (and has) been argued, "what's the point, that's just complicating the code?" And yes, passing in via parameters, data that's accessible via the object's scope, does add complexity to the code. But storing that data in an object, rather than making it globally accessible, adds complexity too. Yet few would argue that global variables are always better because they are simpler. The point is, the benefits that "tell, don't ask" brings, outweighs that complexity of reducing scope. This applies more so to passing in via parameters than restricting scope to the object. Make that method private static and pass everything it needs in via the parameters and now that method can be trusted to not sneakily access things it shouldn't. Further, it encourages keeping the method small, otherwise the parameter list gets out of hand. And it encourages writing methods that fit the "pure function" criteria.

So I do not see "pure functional" and "tell, don't ask" as opposite to each other. The former is the only full implementation of the latter as far as I'm concerned. Fowler's approach is not complete "tell, don't ask".

But it's important to remember this "full implementation of tell don't ask" truly is an ideal, ie pragmatism must to come into play less we become idealistic and thus treat it wrongly as the only possibly right approach ever. Very few apps can get even close to being 100% side effect free for the simple reason that they'd do nothing useful if they were truly side effect free. We need changing state, we need IO etc for the app to be useful. And in such cases, methods must cause side effects and so cannot be pure. But the rule of thumb here is to keep these "impure" methods to a minimum; only have them have side effects because they need to, rather than as the norm.

It seems reasonable to me to have a battle state. Since this is a turn based game, I keep battle states in a dictionary (multiplayer - there may be many battles played by many players at the same time). Whenever a player makes their turn, I call an appropriate method on the battle state which (a) modifies the state accordingly and (b) returns updates to the players, which get serialized into JSON and basically just tell them what has just happened on the board.

It seems more than reasonable to have a battle state to me; it seems essential. The whole purpose of such code is to handle requests to change state, manage those state changes and report them back. You could handle that state globally, you could hold it inside individual player objects or you could pass it around a set of pure functions. Which one you choose comes down to which works best for your particular scenario. Global state simplifies the design of the code, and is fast, which is a key requirement of most games. But it makes the code way harder to maintain, test and debug. A set of pure functions will make the code more complex to implement and risks it being too slow due to excessive data copying. But it'll be the simplest to test and maintain. The "OO approach" sits half way between.

The key is: there is no one perfect solution that works all the time. The aim of pure functions is to help you "fall into the pit of success". But if that pit is so shallow, due to the complexity it can bring to the code, that you don't so much fall into it as trip over it, then it's not the right approach for you. Aim for the ideal, but be pragmatic and stop when that ideal isn't a good place to go this time.

And as a final point, just to reiterate: pure functions and "tell, don't ask" are not opposites at all.

8

OK - I could make a method RETURN a battle state instead of modifying it in place if I really wanted to.

Yes, that's the idea.

Will I then have to copy everything in the battle state to return a wholly new state instead of modifying it in place?

No. Your "battle state" could be modeled as an immutable data structure, which contains other immutable data structures as building blocks, maybe nested in some hierarchies of immutable data structures.

So there could be parts of the battle state which don't have to be changed during one turn, and others which have to be changed. The parts which don't change do not have to be copied, since they are immutable, one only needs to copy a reference to those parts, without any risk of introducing side effects. That works best in garbage collected language environments.

Google for "Efficient Immutable Data Structures", and you will surely find some references how this works in general.

It seems so much simpler for me to just modify the state in place and return updates.

For certain problems, this can be indeed simpler. Games and round-based simulations may fall under this category, given large part of the game state changes from one round to another. However, the perception of what is really "simpler" is to some degree subjective, and it depends also a lot on what people are used to.

6

For anything, ever said, there exists a context, in which you can put that statement, that will make it absurd.

enter image description here

Uncle Bob is completely wrong if you take the zero argument advice as a requirement. He is completely right if you take that to mean every additional argument makes the code harder to read. It comes at a cost. You don't add arguments to functions because it makes them easier to read. You add arguments to functions because you can't think of a good name that makes the dependency on that argument obvious.

For example pi() is a perfectly fine function as it is. Why? Because I don't care how, or even if, it was calculated. Or if it used e, or sin(), to arrive at the number it returns. I'm fine with it because the name tells me all that I need to know.

However, not every name tells me all I need to know. Some names don't reveal important to understand information that controls the behavior of the function as well as exposed arguments do. That right there is what makes the functional style of programming easier to reason about.

I can keep things immutable and side effect free in a completely OOP style. Return is simply a mechanic used to leave values on the stack for the next procedure. You can stay just as immutable using output ports to communicate values to other immutable things until you hit the last output port that finally has to change something if you want people to be able to read it. That's true of every language, functional or not.

So please don't claim that Functional Programming and Object Oriented Programming are "fundamentally incompatible". I can use objects in my functional programs and I can use pure functions in my OO programs.

However, there is a cost to mixing them: Expectations. You can faithfully follow the mechanics of both paradigms and still cause confusion. One of the benefits of using a functional language is that side effects, while they must exist to get any output, are put in a predictable place. Unless of course a mutable object is accessed in an undisciplined way. Then what you took as a given in that language falls apart.

Similarly, you can support objects with pure functions, you can design objects that are immutable. The problem is, if you don't signal that the functions are pure or the objects are immutable then people get no reasoning benefit from those features until they've spend a lot of time reading the code.

This is not a new issue. For years people have coded procedurally in "OO languages" thinking they're doing OO because they use a "OO language". Few languages are that good at keeping you from shooting yourself in the foot. For these ideas to work they have to live in you.

Both provide good features. You can do both. If you're brave enough to mix them then please label them clearly.

0

I struggle at times to make sense of all the rules of various paradigms. They are at times at odds with one another as they are in this situation.

OOP is an imperative paradigm which is about running with scissors in the world where dangerous things happen.

FP is a functional paradigm in which one finds absolute safety in pure computation. Nothing happens here.

However, all programs must bridge into the imperative world to be useful. Thus, functional core, imperative shell.

Things get confusing when you start defining immutable objects (ones whose commands return a modified copy rather than actually mutate). You say to yourself, "This is OOP," and "I'm defining object behavior." You think back to the tried and tested Tell, Don't Ask principle. Trouble is, you're applying it to the wrong realm.

The realms are entirely different and abide different rules. The functional realm builds up to the point where it wants to release side effects into the world. For those effects to be released, all of the data that would have been encapsulated in an imperative object (had this been written that way!) needs to be at the avail of the imperative shell. Without access to this data that in a different world would have been hidden via encapsulation, it can't do the work. It's computationally impossible.

Thus, when you're writing immutable objects (what Clojure calls persistent data structures) remember you're in the functional domain. Throw Tell, Don't Ask out the window, and let it back in the house only when you re-enter the imperative realm.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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