Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
July 11, 2021 12:56 pm GMT

Why Functional Programmers Avoid Exceptions

If youre in a hurry, here is the 60 second version:

My previous article caused a variety of consternation, imperative patriotism, and lots of nuanced follow up. It reminded me of when Richard Feynman was asked to define how magnets work and he refused. The perturbed interviewer postulated it was a reasonable question in hopes to understand why Mr. Feynman wouldnt answer it. Richard Feynman covered a variety reasons, 2 of which were:

  1. you have to know the deeper reasons first before I can explain it
  2. I cant cheat by using analogies that they themselves require deeper meanings to explain how _they_ work.

In the case of avoiding async/await keywords in JavaScript, this makes a huge assumption you know about Functional Programming, Imperative, exception handling, how various languages approach it or dont, the challenges between dynamic and strongly typed languages, and on and on.

In this article, I wanted to remedy that and focus on the deeper reasons why, specifically being programmatic around how Functional Programmers get things done vs. the theory or whys. This means understanding:

  • why pure functions are preferred
  • how theyre easier to test
  • why you return errors as values using Result/Either types
  • how you compose software using them

Pedantic or Mathematical Answer

In investigating specifically why exceptions arent preferred in Functional Programming, I found out, they arent actually anti-functional programming. Worse, I found out many argue they do not violate pure functions or referential transparency with a lot of fascinating supporting evidence. A few argue they arent even side effects. It gets more confusing when you start comparing strictly typed functional languages vs. dynamic ones, or practicing FP in non-FP languages.

In practice, exceptions, like side effects, seem to violate all the reasons why you use pure functions: Your code is predictable, easier to test, and results in better software. Exceptions ensure your code is unpredictable, reduces the value of the tests, and results in worse software. Yet thats not what the mathematical definitions say. They dont agree, nor disagree with my assertions; rather they just say that known exceptions do not violate referential transparency. Yes, there are detractors. Regardless, this really shook my faith.

One could say these are pedantic; citing the true definition of referential transparency the mechanisms behind how Exceptions can or cannot negatively affect it, and thus possibly not violate pure function rules. However, this is the common problem between scientists and engineers: while scientists will give you the Mathematicians Answer, they wont help you do your actual job.

And thats what brought me back to reality. Im not here to debate semantics, Im here to deliver working software. However, I will cede to nuance if someone wishes to delve into the relationships between the mathematics behind these constructs. So far, preferring mathematical style programming over Imperative or Object Oriented seems to be going much better in delivering better results even if I dont have a 100% iron clad understanding of all the nuances of the rules.

The good news, despite finding deep nuance around exceptions and their complicated relationship with the mathematical purity of FP the industry, both FP and others (i.e. Go, Rust, Lua) has basically accepted the pragmatic truth: exceptions arent pure, act like side effects, and arent helpful when writing software. We already have a solution: returning the errors as values from functions, using Result (or Either) types.

Keep in mind, the above has a Haskell bias. I encourage you to google Exceptions Considered Harmful and see some of the horrors that can arise when exceptions put your stateful code (Java/C#/Python/JavaScript) into a bad state.

Prefer Pure Functions

When people say prefer pure functions its because of the following reasons:

  • more predictable
  • easier to test
  • easier to maintain

What does that mean, though?

Predictable

We say predictable because you call it and it returns a value. Thats it.

const isAnOk = safeParseJSON('{"foo": "bar"}')const isAnError = safeParseJSON('')

When you bring exceptions into it, you now have 2 possibilities: it either returns a value, or blows up.

const result = JSON.parse('') // result is never used/set

When you combine functions together into programs, the program takes a value and returns a value. Thats it.

When you bring exceptions into it, you now have X * Y possibilities: the program either returns a value, or X number of functions possibly explode in Y number of ways; it depends on how you wire the functions together.

This exponential complexity shows just how unpredictable code can be with exceptions.

Easier To Test

Easier compared to what? How?

Pure functions dont have side effects, so you dont have to setup and tear down stubs or mocks. There is no initial state to setup, nor state to reset afterwards. There is no spy that you have to assert on after you call your code.

Instead, you give your function an input, and assert the output is what you expect.

expect(safeParseJSON('{"foo": "bar"}')).to.be(Ok)expect(safeParseJSON('')).to.be(Error)

Easier to Maintain

Compared to what? What does easier mean? Easy for someone familiar with the code? This statement is too nebulous and full of feelings.

Still, many would agree, regardless of language, that code that doesnt have any side effects is a lot easier to deal with and change and unit test over 6 months of the code growing compared to one that has a lot of side effects that you have to account for, test, and learn about their possible exponential changes in the code.

Use Result/Either

If you prefer pure functions, that means very little side effects, or theyre on the fringes of your code. But then how do you handle things that go wrong? You return if the function worked or not. If it worked, itll have the data inside. If it failed, itll have a reason why it failed. In FP languages they have a Result or Either type. In languages that dont have this kind of type, you can emulate in a variety of ways. If the code works, you return an Ok with the value in it. If the function failed, you return an Error with the reason why as a string clearly written in it.

const safeParseJSON = string => {    try {        const result = JSON.parse(string)        return Result.Ok(result)    } catch(error) {        return Result.Error(error?.message)    }}

Many languages have embraced the Promise, also called a Future, way of doing things. Some languages have used this to also handle asynchronous operations because they can fail in 2 ways that mean the same thing: it broke or it timed out. For example, most people arent going to wait 10 minutes for their email to come up, so you typically will see failures within 10 to 30 seconds even though technically nothing went wrong; we just stopped trying after a set amount of time. JavaScript and Pythons versions dont have this timing built in, but there are libraries that allow to use this behavior.

This results in pure functions that always return a value: a Result. That can either be a success or failure, but its always a Result. If its a failure it wont break your entire program, nor cause you to have to write try/catch. While Promises can substitute in for a Result in JavaScript for example, ensure you are using the Promise itself, and not the value it returns via async/await. That completely bypasses the built-in exception handling, and forces you to use try/catch again.

Composing Programs

The way you build FP programs is through combining all these pure functions together. Some can be done imperatively, sure, but most are done via some type of railway oriented programming. There are variety of ways to do this in FP and non-FP languages:

This means, in ReScript and F#, youll have a function, and a Result will come out. You can then see if your program worked or not.

let parsePeople = str =>    parsePeopleString(str) // <-- this function could be an Ok or Error    -> filterHumans    -> formatNames    -> startCaseNames

For JavaScript/Python, its a bit more nuanced around the types. For Python, well assume youre returning a Result in PyMonad or Returns.

def parse_people(str):  return parse_people_string(str)  .then(filter_humans)  .then(format_names)  .then(start_case_names)

Composing JavaScript via Promises

For JavaScript, unless youre all-in on some kind of library, natively you can do this using Promise. Promise is already a type of Result: it holds a value, and if it worked, you can get it out using then, else the failure via catch. Theyre also composable by default so you can create Promise chains that automatically unwrap Promise values, use regular values as is, or abort to the catch in case of an error. You lose that ability once you start using async await because now youre responsible for:

  • exception handling
  • pulling the value out
  • if its a Promise, async/awaiting it
  • if its a value, using it
  • putting into the next function down the line
  • handling what to do if you get an exception at each section of the code

For Promises, you just return a value or another Promise and it just comes out the other end ready to go. If not, youre catch will handle any errors. This ensures whatever function calls your Promise chain itself is pure because it always returns a Promise value.

2 huge assumptions:

  1. youre always defining a catch
  2. youre not using a Result

Mixing in Result

If some functions arent asynchronous, most JavaScript programmers would think they can just return a Result type instead to keep it synchronous. There isnt a huge penalty in speed/memory to using a Promise, but some would prefer to use a Result instead. Id suggest to 2 things if youre not using a library: favor a Promise over a Result. A Promise is native and basically acts like a result already.

const parseJSONSafe = string => {  try {    const result = JSON.parse(result)    return Promise.resolve(result)  } catch(error) {    return Promise.reject(error)  }}

If, however, youd prefer to make a clear delineation between an async operation and a possible failure scenario, then youll have to unwrap it at the end of the promise chain, similar to Rust or Pythons dry/returns. There are many helper methods on how to do this based on what Result library youre using. Well use Folktale below. Here weve defined a safe wrapper around JSON.parse:

const parseJSONSafe = string => {  try {    const result = JSON.parse(result)    return Ok(result)  } catch(error) {    return Failure(error)  }}

When using it, itll come out the next Promise then and we can pattern match to get the error or value out and convert to a normal Promise.

const parse = () =>  fetchJSON()  .then(parseJSONSafe)  .then(    result =>      result.matchWith({        Failure: ({ value }) => Promise.reject(new Error(value)),        Ok: ({ value }) => Promise.resolve(value)  )

Conclusions

Functional Programmers avoid exceptions because they basically act like side effects, tend to feel like theyre violating pure function rules in regards to having no return value and possibly crashing our program. If you instead favor pure functions, return a Result type when things can possibly fail. You can then use your languages preferred way of composing functions together. Then you have pure programs that have an input and an output. This means both the functions, and the program itself, are much easier to unit test. You no longer have to write expect(thisThing).throws(SomeExceptionType). You dont have to write try/catch/throw in your code. You just give your functions or program and input, and assert on that output.

For side effects, if you cant force them to return a meaningful value, then you can just assert they were called with your expected inputs via Sinons spy methods or TestDoubles assert method. There is no longer indirection, no longer a need to use to try/catch in multiple places for your code. This makes your functions and program much more predictable, especially when you combine many functions together.

For native functionality in non-functional languages like JavaScript and Python, you wrap the unsafe code. In the above examples, we wrapped JSON.parse with a try/catch and have it either return a Result or Promise. In FP languages, that would already return a Result. If youre programmatic, languages like ReScript and F# support both Result types AND pattern matching on exceptions (which I think is blasphemy).


Original Link: https://dev.to/jesterxl/why-functional-programmers-avoid-exceptions-8oe

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To