Hacker News new | ask | show | jobs
by z9znz 1296 days ago
Unfortunately this toy example isn't even pure functional because it raises an exception as a way of providing an alternative return from the function.

A better example, if one wanted to keep a single function pure and still keep (two) states would be to return a tuple of (current_count, is_expired). Further, since it uses zero as the implicit boundary to determine when the counter is complete by counting down from the provided value, the function should be named countdown_timer() or something more descriptive.

Honestly I'm not sure who this little article was written for. The references to C++ were irrelevant. The OOP Python example, however, was a pretty common example of how Python people write code. It's a shame, really. We should (edit: not) have to deprogram people to get them to choose something more like the latter "functional" approach over the former pointlessly-OOPy example.

1 comments

Why do you consider raising an exception not being pure functional? In my opinion, it is exactly equivalent to propagating error return value up the call chain until it is handled (but is faster if no exception is thrown, and contains a stack trace of where the original error value was produced for programmer's convenience).
If you take that view, yes, it's functional.

However, if you take that sort of view, everything is functional. Writing to the screen can be viewed as having returned instructions to write to the screen, combined with an interpreter that performs it. Writing to the network is like having returned an instruction to write the network, along with an interpreter to do it. (And a certain laissez faire attitude about laziness, perhaps.)

This view is not necessarily wrong. There is a certain truth to it; for one thing it's not a terrible gateway to understanding how "pure functional" languages interact with the real world in practice, because this is pretty close to how they do it.

However, it is useless, because it means every language is a pure functional language, and the utility of a term is its ability to split the universe under discussion into multiple pieces. Or to put it another way, a decision procedure that puts 100% of the items under decision into the same category conveys exactly zero bits of information.

So for a definition of "pure functional" to have any utility, it needs to exclude some things. Exceptions are borderline. Technically Haskell does indeed have them, but it has a complicated relationship with them. It is certainly the case that we are discussing something that is not in the function signature anymore, which people generally consider not "pure functional". But generally it definitely has the "well, we could pretend that we returned a special value and had a special interpreter wrapped around it for every call" copout feel to it in general.

I am not trying to argue, rather being curious.

I've tried to look up what "pure functional" means exactly, and failed to find anything better than Wikipedia[0]:

    In computer programming, a pure function is a function that has the following properties:
    1. the function return values are identical for identical arguments (no variation with local static variables, non-local variables, mutable reference arguments or input streams), and
    2. the function has no side effects (no mutation of local static variables, non-local variables, mutable reference arguments or input/output streams).
I don't see how that excludes exceptions. I agree that exceptions kind of "feel" imperative rather than functional, but could you maybe help me find an accepted definition which excludes exceptions?

[0] https://en.wikipedia.org/wiki/Pure_function

I think it excludes exceptions: exceptions are a side effect
Raising an exception is not returning a value; it is essentially a GOTO.

A pure function has no side effects. Maybe it takes an input, and maybe it returns a result. But whatever it does internally has no affect on the outside world. Raising an exception is definitely a side effect.

Worse yet, in TFA example it wasn't even necessary to use an exception in place of a returned state. I would argue that the exception example was, irrespective of functional vs imperative principles, a bad use of exceptions.

Yeah, I agree that in that Python example, use of exceptions is awful (and the entire example is awful, I think).

> But whatever it does internally has no affect on the outside world. Raising an exception is definitely a side effect.

However, would you please elaborate, how do exceptions affect the outside world, in your opinion? As far as I understand, exceptions only "return exceptional value" up the call stack, to the calling function. Of course, barring total program crash in case of unhandled exception, but unhandled exception is a programmer's error, like integral division by zero or infinite memory allocation loop.

Raising an exception affects control flow, which is the execution state of the program.

TFA example misuses exceptions as just another return value, requiring the caller to have additional code to handle "normal" and "other" return values. So as we and others seem to agree, that is not even a good example. At this point we kind of get stuck in the whole discussion about exceptions and how to handle them. This is probably a hot area that I should avoid getting into, but here is an _example_ of how to handle errors without having exceptions (and without impacting execution states): https://go.dev/blog/error-handling-and-go

Function application, where you apply a function to a collection of data, requires that the application function returns a value (and does not throw an exception). As a Python example (not the prettiest, but it's Python...), imagine you have a list of strings which you want to strip the leading and trailing whitespace.

x = [' a', 'b ', None]

list(map(str.strip, x)) => TypeError exception because you can't strip on None

To work around this, you have to do something like an anonymous function to validate your inputs before calling strip:

list(map(lambda s: s.strip() if s is not None else None, x)) => ['a', 'b', None]

In this case it's clear why Python throws an exception, and we were able to avoid it by validating the input during the mapping. But if a normal behavior of the mapped function was to throw an exception as just another type of result, we couldn't prevent/avoid that, and we would either not be able to apply the timer() function to a list of inputs or we would have to have a form of map() which also accepted an exception handler function and then somehow wove its results into the normal map output. Such a thing may exist, but I don't know Python well enough; and if it does exist, it would only make the language even uglier ;). [Edit: on second thought, the solution would be to make a better_timer() function wrapper which called timer() and handled the exception, then more appropriately returned [time_remaining, complete_flag]. Then we would pass better_timer() to the map().]

There are even nicer benefits to purity if you are using higher order functions that may be called later/elsewhere. Instead of taking data through some series of transformations, you take data and build a series of operations which can later be used in a transformation. Then somewhere more toward the edge, well beyond your core business rules, you execute the series of transformations (functions) on your starting data to get an end result. This approach can't be used all the time, but there are cases where it works well. But like the map() example above, if you have to also handle exceptions as normal results returned from functions, then you have to have additional exception handler functions that you carry around with the other functions in case they are needed. Yuck.

Lean maps returns into exceptions.

https://leanprover.github.io/about/