Skip to main content
added 52 characters in body
Source Link
Ben
  • 1k
  • 7
  • 10

To remain "pure" all possible ways of generating the bottom value have to be treated as equivalent. That includes the "bottom value" that represents an infinite loop. Since there's no way to know that some infinite loops actually are infinite (they might finish if you just run them for a bit longer), you can't examine any property of a bottom value. You cancan't test whether something is bottom, can't compare it to anything else, can't convert it to a string, nothing. All you can do with one is put it places (function parameters, part of a data structure, etc) untouched and unexamined.

Python already has this kind of bottom; it's the "value" you get from an expression that throws an exception, or doesn't terminate. Because Python is strict rather than lazy, such "bottoms" can't be stored anywhere and potentially left unexamined. Any expression containing a bottom in this sense will automatically evaluateSo there's no real need to use the same kindconcept of the bottom (the same exceptionvalue to explain how computations that fail to return a value can still be treated as if one was thrown, because it propagates outward, or the same infinite loop)they had a value. But there's also no reason you couldn't think this way about exceptions if you wanted.

Throwing exceptions is actually considered "pure". It's catching exceptions that breaks purity (precisely- precisely because it allows you to inspect something about certain bottom values, instead of treating them all interchangeably. In Haskell you can only catch exceptions in the IO that allows impure interfacing (so it usually happens at a fairly outer layer). Python doesn't enforce purity, but you can still decide for yourself which functions are part of your "outer impure layer" rather than pure functions, and only allow yourself to catch exceptions there.

To remain "pure" all possible ways of generating the bottom value have to be treated as equivalent. That includes the "bottom value" that represents an infinite loop. Since there's no way to know that some infinite loops actually are infinite (they might finish if you just run them for a bit longer), you can't examine any property of a bottom value. You can test whether something is bottom, can't compare it to anything else, can't convert it to a string, nothing. All you can do with one is put it places (function parameters, part of a data structure, etc) untouched and unexamined.

Python already has this kind of bottom; it's the "value" you get from an expression that throws an exception, or doesn't terminate. Because Python is strict rather than lazy, such "bottoms" can't be stored anywhere and potentially left unexamined. Any expression containing a bottom in this sense will automatically evaluate to the same kind of bottom (the same exception if one was thrown, because it propagates outward, or the same infinite loop).

Throwing exceptions is actually considered "pure". It's catching exceptions that breaks purity (precisely because it allows you to inspect something about certain bottom values, instead of treating them all interchangeably. In Haskell you can only catch exceptions in the IO that allows impure interfacing (so it usually happens at a fairly outer layer). Python doesn't enforce purity, but you can still decide for yourself which functions are part of your "outer impure layer" rather than pure functions, and only allow yourself to catch exceptions there.

To remain "pure" all possible ways of generating the bottom value have to be treated as equivalent. That includes the "bottom value" that represents an infinite loop. Since there's no way to know that some infinite loops actually are infinite (they might finish if you just run them for a bit longer), you can't examine any property of a bottom value. You can't test whether something is bottom, can't compare it to anything else, can't convert it to a string, nothing. All you can do with one is put it places (function parameters, part of a data structure, etc) untouched and unexamined.

Python already has this kind of bottom; it's the "value" you get from an expression that throws an exception, or doesn't terminate. Because Python is strict rather than lazy, such "bottoms" can't be stored anywhere and potentially left unexamined. So there's no real need to use the concept of the bottom value to explain how computations that fail to return a value can still be treated as if they had a value. But there's also no reason you couldn't think this way about exceptions if you wanted.

Throwing exceptions is actually considered "pure". It's catching exceptions that breaks purity - precisely because it allows you to inspect something about certain bottom values, instead of treating them all interchangeably. In Haskell you can only catch exceptions in the IO that allows impure interfacing (so it usually happens at a fairly outer layer). Python doesn't enforce purity, but you can still decide for yourself which functions are part of your "outer impure layer" rather than pure functions, and only allow yourself to catch exceptions there.

Source Link
Ben
  • 1k
  • 7
  • 10

Haskell semantics uses a "bottom value" to analyse the meaning of Haskell code. It's not something you really use directly in programming Haskell, and returning None is not at all the same kind of thing.

The bottom value is the value ascribed by Haskell semantics to any computation that fails to evaluate to a value normally. One such way a Haskell computation can do that is actually by throwing an exception! So if you were trying to use this style in Python, you actually should just throw exceptions as normal.

Haskell semantics uses the bottom value because Haskell is lazy; you're able to manipulate "values" that are returned by computations that haven't actually run yet. You can pass them to functions, stick them in data structures, etc. Such an unevaluated computation might throw an exception or loop forever, but if we never actually need to examine the value then the computation will never run and encounter the error, and our overall program might manage to do something well-defined and finish. So without wanting to explain what Haskell code means by specifying the exact operational behaviour of the program at runtime, we instead declare such erroneous computations produce the bottom value, and explain what that value behaves; basically that any expression which needs to depend on any properties at all of the bottom value (other than it existing) will also result in the bottom value.

To remain "pure" all possible ways of generating the bottom value have to be treated as equivalent. That includes the "bottom value" that represents an infinite loop. Since there's no way to know that some infinite loops actually are infinite (they might finish if you just run them for a bit longer), you can't examine any property of a bottom value. You can test whether something is bottom, can't compare it to anything else, can't convert it to a string, nothing. All you can do with one is put it places (function parameters, part of a data structure, etc) untouched and unexamined.

Python already has this kind of bottom; it's the "value" you get from an expression that throws an exception, or doesn't terminate. Because Python is strict rather than lazy, such "bottoms" can't be stored anywhere and potentially left unexamined. Any expression containing a bottom in this sense will automatically evaluate to the same kind of bottom (the same exception if one was thrown, because it propagates outward, or the same infinite loop).

Throwing exceptions is actually considered "pure". It's catching exceptions that breaks purity (precisely because it allows you to inspect something about certain bottom values, instead of treating them all interchangeably. In Haskell you can only catch exceptions in the IO that allows impure interfacing (so it usually happens at a fairly outer layer). Python doesn't enforce purity, but you can still decide for yourself which functions are part of your "outer impure layer" rather than pure functions, and only allow yourself to catch exceptions there.

Returning None instead is completely different. None is a non-bottom value; you can test if something is equal to it, and the caller of the function that returned None will continue to run, possibly using the None inappropriately.

So if you were thinking of throwing an exception and want to "return bottom" to emulate Haskell's approach you just do nothing at all. Let the exception propagate. That's exactly what Haskell programmers mean when they talk about a function returning a bottom value.

But that's not what functional programmers mean when they say to avoid exceptions. Functional programmers prefer "total functions". These always return a valid non-bottom value of their return type for every possible input. So any function that can throw an exception isn't a total function.

The reason we like total functions is that they are much easier to treat as "black boxes" when we combine and manipulate them. If I have a total function returning something of type A and a total function that accepts something of type A, then I can call the second on the output of the first, without knowing anything about the implementation of either; I know I'll get a valid result, no matter how the code of either function is updated in future (as long as their totality is maintained, and as long as they keep the same type signature). This separation of concerns can be an extremely powerful aid for refactoring.

It's also somewhat necessary for reliable higher order functions (functions that manipulate other functions). If I want to write code that receives a completely arbitrary function (with a known interface) as a parameter I have to treat it as a black box because I have no way of knowing which inputs might trigger an error. If I'm given a total function then no input will cause an error. Similarly the caller of my higher order function won't know exactly what arguments I use to call the function they pass me (unless they want to depend on my implementation details), so passing a total function means they don't have to worry about what I do with it.

So a functional programmer that advises you to avoid exceptions would prefer you instead return a value that encodes either the error or a valid value, and requires that to use it you are prepared to handle both possibilities. Things like Either types or Maybe/Option types are some of the simplest approaches to do this in more strongly typed languages (usually used with special syntax or higher order functions to help glue together things that need an A with things that produce a Maybe<A>).

A function that either returns None (if an error happened) or some value (if there was no error) is following neither of the above strategies.

In Python with duck typing the Either/Maybe style is not used very much, instead letting exceptions be thrown, with tests to validate that the code works rather than trusting functions to be total and automatically combinable based on their types. Python has no facility for enforcing that code uses things like Maybe types properly; even if you were using it as a matter of discipline you need tests to actually exercise your code to validate that. So the exception/bottom approach is probably more suited to pure functional programming in Python.