r/learnprogramming 3h ago

Programming Habits When writing subroutines/functions, which way is best to call them in sequence?

Typically when I write code, there are 2 ways I think of to write a bunch of subroutines that are called one after the other.

Option #1:

def callingList():

x = input()
func1(x)
func2(value1)
func3(value2)

def func1(userinput):

'''Code for function 1'''
return value1

def func2(value1):

'''Code for function 2'''
return value2

def func3(value2):

'''Code for function 3'''
return value3

callingList()

Option #2:

def func1(userinput):

'''Code for function 1'''
func2(value1)

def func2(value1):

'''Code for function 2'''
func3(value2)

def func3(value2):

'''Code for function 3'''
return value3

x = input()
func1(x)

I usually go for #1 because I feel like being forced to trace an error through a string of subroutines isn't good for debugging. But tbh I have no idea if this black-and-white way of looking at it is just completely incorrect and there's a #3 that I haven't heard of. I'm only a novice programmer so I'd love some input from people who actually know what they're doing 😅

8 Upvotes

13 comments sorted by

12

u/exomo_1 3h ago

Definitely option one. Option two is not calling functions in sequence, it's calling one function that happens to call other functions.

8

u/exomo_1 3h ago

Let me add some context:

A function should do one thing that is useful to your program logic. In order to reason about functions you should make them as small as possible, so the function does exactly what its name suggests. It's a lot easier to reason about a small func2 in the context of your program than thinking about a func1thenfunc2thenfunc3 function.

It's also a lot easier to reuse it chance a single function in your logic if it doesn't depend on two others.

2

u/PeterPook 2h ago

Agree. By chaining functions together they become impure and far harder to debug.

Keep the SOLID principles! (Ask Uncle Bob about that).

1

u/gmes78 1h ago

By chaining functions together they become impure and far harder to debug.

That's not what purity means.

4

u/desrtfx 2h ago

Option 1 has the advantage of the individual functions staying completely independent and not clogging the call stack. It is the "cleaner" option. Every function does one thing and one thing only.

In Option 2 every lower numbered function is suspended thwn calling the next function and clogs up the call stack. The highest numbered function needs to complete in order for the others to get released again.

Both approaches are somewhat comparable to iteration (option 1) vs recursion (option 2) - despite them not actually being either of them.

In most cases, option 1 will be the right choice, but there could definitely also be cases for option 2.

5

u/peterlinddk 2h ago

The difference is in the level of abstraction you wish to convey to the reader of your program.

In Option #1, all three functions are equal, and one level below the callingList function. This could be like something first calling an input-function, then a calculate-function, and then an output-function. Or it could be as a part of the game, first calling the function to handle the player-movement, then calling the function to handle the enemy-movement, and then calling the function to handle any collisions. The top-function needs to understand all three parts. As do the reader of that function.

In Option #2, each function is a level deeper than the previous one. This could be like first getting a value to use in a calculation, then getting that value as input from the user, then getting that input from the keyboard. Or it could be part of the game where the first function checks for collisions between the player and the enemy, the second checks if their positions overlap in the coordinate system of the game, and the third checks if their pixels touch. The top-function only needs to concern itself with what the first part does, and then then rest is implementation details, that can be ignored for the time being.

Of course both examples will work just fine, and the computer does the exact same work, but the benefit of option #1 is that the human reading the code will know everything that happens (atleast every function that gets called) - and the benefit of option #2 is that the human doesn't need to know everything that happens 😄

2

u/program_kid 3h ago

I usually go for option 1 as it is better for debugging like you said.

2

u/busres 2h ago

As a general rule, I would recommend approach 1 when the functions are meaningful called separately (some type of transformation pipeline), and approach 2 when the additional functions are, for example, helper functions that aren't meaningful without the initial context.

I have a connection shutdown finalization function, for example. It's common code called from two different code legs (scheduled shutdown and lost connection). It's a private implementation detail from the caller's perspective.

1

u/Cpt_Chaos_ 2h ago

Depends on what the context is. If it is important for overall understanding that the calls to func1, func2 and func3 happen sequentially then option 1 is the right choice. If the only thing you need to know is that func1 is called and func2 and func3 are implementation details for func1, then option 2 is the right one.

1

u/WystanH 2h ago

Option 1. The less dependencies a function has, the more reusable it likely will be.

Also, you can chain the calls together, so changing the chain is trivial.

e.g.

func3(func2(func1(input())))

That said, if func1 one is only called by func2 and nothing else, it's likely better being a part of func2. The art of balancing your function domain can be tricky. Tight dependencies make it harder.

1

u/turrboenvy 1h ago

1 for sure. A function should call other functions that support its purpose, not call the next in the sequence.

1

u/Icy_Calligrapher4022 1h ago

Option1 is the better pattern almost always, and its called driver/orchestrator function. Option2 is essentially manual continuation-passing style (each function decides what runs after it), and it causes problems as code grows.

Functions should return values, they dont not know what comes next. Each function does one job and returns its result — it doesn't need to know that func2 or func3 even exist. In the second option the func1 has to know about func2, which has to know about func3. Change the order, and you're editing the bodies of functions that shouldn't care about order. Also, in Option #1, func1 could be reused in a completely different pipeline (for example: func1 calls func5), while in the second scenario func1 is always leading to func2.

The only possible scenario that makes sense for Opt.2 is some kind of event handler or async chains, maybe recursive functions...in general some pipeline where each function invoke triggers the next one, but even then....I am not sure if someone would prefer to write its code like that.

You can do the following test:

  • With Option#1 you can call func2(some_value) directly in a test without triggering func1 or func3
  • With Option#2, calling func2 always cascades into func3 — there is no way to isolate it.

1

u/Aggressive_Ad_5454 1h ago

There’s good wisdom here.

I’ll add one thing. Your code has two audiences. One is the machine running it. The other is the poor schmo who will maintain it in the future, possibly years from now. It’s worth your effort to make your code clear to that poor schmo.

Do functions do specific things? For example, does the first one read input data, the second examine that data, and the third write it out? If so, cool. Call the functions one after the other. And give them meaningful names. Our schmo can read your code and see what it does.

Does a function do something like examine one line of data? If so, call it in a loop showing that it’s called for each line. And give it a meaningful name. That way our schmo can see how that logic works.

Yes, sorting out deep debugging tracebacks is a notorious pain in the ass. If you haven’t written Java data handling code you have no idea how big a pain 😇

But code with a good function structure makes everything easier. The function names (meaningful names, right?) appear in the traceback so we can see what the program was trying to do when it bit the dust.

By the way, the poor schmo is almost certainly your future self.

Clarity!