2

Suppose we have a function with the following structure:

def f():
    # ...
    # some computations
    # ...
    if something_is_wrong_with_previous_computations:
        return None
    # ...
    # some other computations
    # ...
    if something_is_wrong_with_previous_computations2:
        return some_variable
    #...
    return result

As I see it, using return statement in the middle of a function is not functional at all. If we were in some lispy language we would have the let (then computations could be written with let*) statement, which would help us deal with these situations with ease. Unfortunately, we don't have it here. What should we do?

  • Simulate let with creating lots of nested functions and calling them in place?
  • Use something like Maybe monad or another complex stuff like that?
  • Don't waste our time and write it imperatively?
  • Something else?
7
  • Can you give an example of how you would do this in LISP (any dialect)? I'm curious because I don't see how let can help in this situation and want to know. Commented Mar 15, 2021 at 16:24
  • @YevhenKuzmovych "For great justice" ©. Joking aside, it's more for the sake of learning, then for production Commented Mar 15, 2021 at 16:25
  • @jbmeerkat ok, I agree, it's not about let, but let* (scheme): ``` (define (f) (let* ([v1 #| some-computation |#] [v2 #| some-computation-2 |#] #|etc|#) (if wrong1 Null (let* (#|some-more-computations|#) (if wrong2 some-var (let* ...)))))) ``` (I hope you can read that))) Commented Mar 15, 2021 at 16:37
  • As I can understand, example in Scheme is pretty similar to the code you wrote in your question in Python except returns. I think the key difference here is that in Scheme "returns" are implicit, but in Python they are strictly explicit. You can do it in Python with single return but if will be more like C-style, not functional pastebin.com/xgGD0iBn Commented Mar 15, 2021 at 16:59
  • 2
    The difference is that Lisps are expression languages: Python isn't. My opinion is that if you try to write programs in non-expression-languages as if they were expression languages that ... it's easier to just use an expression language rather than to try and twist the other language into one. Commented Mar 15, 2021 at 18:13

1 Answer 1

5

A return statement anywhere in a function is not functional.

In Python, you have no choice.

The Lisp code

(defun sgnum (x)
  (cond
    ((< x 0) -1)
    ((zerop x) 0)
   (t 1)))

turns into Python as

def sgnum(x):
  if x < 0:
    return -1
  elif x == 0:
    return 0
  else:
    return 1

In Lisp, we know we have deviated from functional coding when we use variable assignment, or the "program feature": an explicit progn construct, or an implicit equivalent, or any of is cousins like prog or prog1. A functional function in Lisp always has a body which is made up of a single expression (or possibly no expressions at all).

You can redefine what you mean by "functional coding" in Python. How about these rules:

  1. Every statement in a "functional function" must be a single statement; it cannot be followed by another statement. Thus, the whole body of a function is a single statement, and in it are embedded single statements.

  2. No statement in the function may allow control to fall through it. Every statement must return. Thus return is not only considered "functional" but essential to achieving this goal.

  3. A variable may be defined, but not redefined. Parallel, mutually exclusive control flows may assign the same variable different values, but no variable can be assigned more than once in the same control flow.

With these kinds of rules, you can get the program to have a control flow graph resembling that a program in the pure Lisp style: a control graph that is basically a tree of decisions with embedded calculations and variable binding, at the leaves of which are values to be returned.

Speaking of variable binding, we should probably have a fourth rule:

  1. A statement may be preceded by a sequence of fresh variable assignments that contain no side effects. Such a sequence, together with the statement which follows it, counts as one statement.

Arguably also a fifth one:

  1. No statement must be used which evaluates any contained expression or statement more than once.

Otherwise we permit loops, which are not functional. This is tricky because some looping constructs are relatively well behaved, like implicitly stepping a dummy variable over the elements of a list. The only way you can tell it's not functional is that a lexical closure captured in the loop will easily reveal there is only one variable being mutated and not a fresh variable being bound for each iteration.

According to these rules, sgnum is "functional": it contains just one if/elif/else statement, which does not allow control to fall through it: every branch returns:

This version of sgnum is not "functional" any more:

def sgnum(x):
  if x < 0:
    return -1
  if x == 0:
    return 0
  return 1

It contains three statements in sequence. Whereas the following is "functional" even though it also consists of three statements:

def distance(x0, y0, x1, y1):
  xd = x1 - x0
  yd = y1 - y0
  return math.sqrt(xd * xd + yd * yd)

These meet the rules. The first two statements bind fresh variables, meeting rule 3, so are permitted by 4 to precede a statement. The return statement meets rules 1 and 2. This is very similar to:

(defun distance (x0 y0 x1 y1)
  (let ((xd (- x1 x0))
        (yd (- y1 y0)))
    (sqrt (+ (* xd xd) (* yd yd)))))

Lastly, note how our rules are at odds with the ancient programming advice of "have only one exit point in a function". That little tidbit you may find in some coding conventions is quite anti-functional. To achieve a single point of return in a nontrivial function requires imperative style control flows through multiple statements and/or variable assignments. From the functional point of view, it is a myopic, silly rule; but it makes sense in those contexts where it is recommended, because it can help improve very poorly structured imperative code.

Sign up to request clarification or add additional context in comments.

1 Comment

Your conventions seem quite reasonable. By returning from if's we're really emulating if from Lisp. Though I would argue on the fifth rule. We can generalize that in the following statement: we MUST permit loops in Python, otherwise any nontrivial logic cannot be expressed in this language. Maps and reduces are good, but sometimes they are totally not enough (like any higher-order function). At least I can't see how to use them without making a huge mess. In a functional language we'd have tail recursion for cases like that. But we don't.

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.