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:
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.
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.
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:
- 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:
- 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.
let, butlet*(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)))