loading...
Cover image for Is an exception less (pure) than a Result?

Is an exception less (pure) than a Result?

misobelica profile image Mišo Updated on ・4 min read

Recently I saw a video about the Railway Oriented Programming and I can't help myself but I ask "why"? So I searched and found Against Railway-Oriented Programming from the same author. Still, I have to ask: "Why should I teach how to compose functions that don't match when I can simply throw an exception and have all of this for free? And it is not just this video. With recent OOP hate and functional passion, I hear it all the time how the explicit result is better because then the function is pure and returns always the same output data for the same input data. And then I come to the question. Is that different from the exceptions?

From what I know the function is pure when it returns the same result for the same arguments. And if the function always throws the same exception for the same input arguments it is pure. Even reducers in Redux are considered pure if they don't have side-effects and they throw exceptions all the time (famous undefined is not an object/function/...). So there has to be something else. Something why one would like to abandon the simplicity of throwing exceptions with the whole railway and even call it a better approach. Below is an example to try to find it.

type Result = 
  | {success: true, processedData: ProcessedData}
  | {success: false, error: string};

/**
 * Pure function with the "Result" type.
 */
function pureFunction(user?: User, data: Data): Result {
  if (!user) {
    return {success: false, error: 'User is not authenticated'}
  }

  return {success: true, processedData: {...data, processed: true}}
}

/**
 * The same function but "returning" an error as an exception.
 */
function maybePureFunction(user?: User, data: Data): ProcessedData {
  if (!user) {
    return throw new UserNotAuthenticated(user)
  }

  return {...data, processed: true}
}

Can you spot the difference? I can. It's hidden in types. I don't know any language out there checking the types of the exceptions. Java somehow tried with the checked exceptions and failed, Python don't plan it. Nobody wants to write all the possible exceptions thrown from every function (sorry, method). And still, it is the same as to write all the return types. So this is a big advantage of the Return type - type safety. The compiler will tell you that you don't handle some possible error but don't warn you about uncaught exceptions.

And then I had to ask again. Why? Why can't we have both advantages? Why can't I just throw an exception and let the compiler infer the exceptions for the function and warn me about possibilities? Then I don't need to learn how to combine functions by bind, >>=, etc. I think the idea of checked/unchecked exceptions in Java is good. Only I would name them BusinessException and ProgrammerError or something like that. And they would be inferred by the compiler.

In fact, I am not afraid to consider exceptions to be algebraic data types, sum types or enums as defined by Rust. Here is my ideal API.

try {
    return maybePureFunction(user, data)
} catch (e) {
    // here the compiler knows "e" is "UserNotAuthenticated"
    return DATA_FOR_ANONYMOUS_USER
}

// OR

// compiler infers here the function return type is kind-of "Result<ResourceNotFound | DangerousError, string>"
function something(fileName: string): string {
    try {
        return dangerousCall(fileName)  // throws "DangerousError" unhandled here
    } catch (e: FileDoesNotExist | InsufficientPermissions) {
        throw new ResourceNotFound(fileName);
    } catch (e: FileLocked) {  // isn't this actually pattern matching?
        return ""
    }
}

What do you think about it? And more importantly, do you know such a language where exceptions are a proper part of the type system and are treated as a result of the function?

Interesting sources

Posted on by:

misobelica profile

Mišo

@misobelica

I am an introvert ☺️

Discussion

pic
Editor guide
 

An exception hampers the function call stack by unwinding it. That's the problem. It is an computational effect that happens outside the realm of a pure function.

So what can we do about it? Your observation is right: A partial function that exhibits the same exception for the same input is deterministic. So we have to turn the effect into a value in order to render this function pure. In the functional paradigm there are Maybe/Option and Either types to encode exceptions. Instead of unwinding the call stack these types short circuit code, which can cause the stack to unwind as well, but this time with the means of pure functions.

 

Thanks for the answer. It forced me to search for stack unwinding and I found some interesting sources and added them at the end of the article. I still don't understand why this is a problem, but I hope it will help me to learn more about it.

From the various discussions, I noticed the problem is actually a reaction to the exception not throwing it. It's that the control flow changes and it's like a GOTO jump. But still, even function call is implemented as a jump in low-level assembly code and that is not a problem. I guess I need to dig deeper.

Have a nice day :)