Who among us has not started with a function like:
def munger(v): v = mungify(v) return doublemungify(v)
Then realized doublemungify goes too far, and changes it to:
def munger(v): v = mungify(v)
But, whoops, now the function returns None. There are many ways to encounter this common bug, to unintentionally return None by not returning anything at all. Maybe one of the execution paths doesn't have a return, or you just forgot about the return at some point. It happens to me often -- but the worst part is that it's not quickly detected. Often I'll collect the results of munger, and the None will only cause a problem sometime later when it's unclear where it came from. Python nit: functions return None when you don't want them to return anything -- None is a oft-used real value, not some this-function-returns-nothing value.
It would be better if functions didn't return any value when no return (or a bare return) when encountered. So using the second munger form in an expression would cause an immediate (and informative) exception. In other words, distinguishing functions from procedures.
It seems like there's some sort of elegance in not making that distinction, in keeping these objects uniform. Plus Pascal distinguishes the two forms, and we all hate Pascal. But really, what use can there be to the implicit returning of None?
Well, one use, usually in subclassing. Supposing munger is a method, we might do:
class SafeGrinder(Grinder): def munger(self, v): if not self.isSafe(v): raise ValueError, "Unsafe: %s" % v return Grinder.munger(self, v)
Because we can always treat functions as, well, functions, we can blindly pass values through. Without this we'd need some sort of special syntax to deal with no return value, or else a way to test whether the object was a function or procedure. The test would be annoying, and would require some static declaration (e.g., two kinds of def). The static declaration would make that pass-through function even harder, because it wouldn't be sufficient to just test whether the function returned a value, we'd also have to declare whether our pass-through code was a function or procedure.
Some other syntax might be better, like (Grinder.munger(self, v) ifprocedure None), which would evaluate to None if Grinder.munger was a procedure, or the return value if not. Then you might do something like:
class OneOff: pass class SafeGrinder(Grinder): def munger(self, v): if not self.isSafe(v): raise ValueError, "Unsafe: %s" % v result = Grinder.munger(self, v) ifprocedure OneOff if result is not OneOff: return result
Or maybe even better:
class SafeGrinder(Grinder): def munger(self, v): if not self.isSafe(v): raise ValueError, "Unsafe: %s" % v try: return Grinder.munger(self, v) except NoReturnValue: pass # and return no value
It can be a little complicated, though only for this one case, while other more common cases would be less error-prone. But it's not really going to happen in this late stage. Oh well.
Here it comes...wait for it...wait for it...unit tests would help here!
If the function which previously returned something meaningful but now returns None were being unit tested, the test would immediately fail. Then you'd only be out about 30 seconds of your life (time between test failing and realizing your mistake). Its a good start at the least.
But if we're going to take the syntactic route, perhaps requiring a return statement would be better. Again, hard to do this late in the game. Yep, once you ship a language you're screwed :)
It strikes me that tests like this are possibly better added to a tool like pychecker, rather than to the language itself.