Functional Programming / Pure functions & Side effects

Pure functions & Side effects in Dart [Functional Programming — Part 1]

In this article, we’ll look at pure functions, side effects in Dart & Flutter, and how they help to minimize unexpected bugs in our code.

Yogesh Parwani
7 min readMar 11, 2022
Pure Functions & Side Effects

I am sure we all have experienced that while changing a piece of code, somewhere something else abruptly starts to break. Frustrating, isn’t it? In this article, we will see how to minimize such unexpected scenarios and reduce the surface area for debugging.

FP is based on lambda calculus and also uses several mathematical concepts from sets and category theory. However, the good news is we don’t have to learn math to understand functional programming. For the scope of this article, we will try to compare the mathematical and programming concepts side by side.

Table of Contents

  • What are Mathematical Functions?
  • What is a Pure Function?
  • What is a Side effect?
  • What are the benefits?
  • What is Referential Transparency?
  • How to use in Real-world applications?

Mathematical Functions

In Mathematics, functions are mappers i.e., they map inputs to outputs. For example, f(x) = 2x is a function that maps the value from one set to its corresponding double value in another set. “A picture is worth a thousand words”, so let’s see an image and try to understand.

We have two sets, f(x) = 2x is the mapper function. Given 1, it’ll always return 2. Given 4, it’ll return 8. Values are mapped from one set to another. Let’s expand these functions a bit more.

Total Function

A function defined for all its inputs is known as a total function. For every input, the function gives a corresponding output. The above image demonstrates a total function. Every input from Set X has a corresponding output from Set Y. In contrast, we have partial functions.

Partial Function

A function that is not defined for some of its inputs is known as a Partial function. Let’s visualize,

Set X is not defined for all its inputs. In our example, it’s not defined for number 3, hence partial.

Okay! Enough of maths, let’s see how things look on the programming side :)

What is a Pure function?

Let’s define a function for starters. In Layman’s terms, Something that either takes zero or more inputs performs some computations and returns an output. Actually, pure functions do the same, but with certain rules.

A pure function is deterministic and has no side effects.

If you’re anything like me and hearing this for the first time, the definition didn’t make much sense. Let’s break it down. A pure function is deterministic, which means it is determined to always give the same output for the same input.

doubleValue is a pure function because it’s deterministic. Given 2, it’ll always return 4. Given 5, it’ll always return 10. No matter at what time of the day, in which sequence or where you run it in your application, it’ll always return the same output for the same input.

If we recollect the definition of total function we can say doubleValue is also a total function as it is defined for all its inputs. No matter what number you give it as input, you’re going to get a result. Does this mean all total functions are pure? Well, technically no and we are going to prove it.

But before, let's cover the second part of the definition that says pure functions have no side effects.

What is a Side effect?

A function is said to have side effects when it deals with external states. External states can be accessing a variable, making API calls, Database transactions, reading/writing on the disk or even logging on to the console. (I am not strict with considering logging as a side effect as we are not in a pure FP language, so I do use it inside function calls without stressing over it)

The function greet is impure as it depends on an external state. Let’s convert it into a pure function.

Now greet function is pure as it does not depend on any external state and will always return the same output for the same input. Let’s take a look at a couple of examples to help us understand better.

  • getDateTime is impure. It always returns a different output.
  • add is pure. For a given input, it always returns the same output. It does not depend on any external state. If I pass 1 and 2, I am guaranteed to get 3 every time. It’s not like on Mondays it’ll return me 3. and on Fridays, it’ll return 8.
  • getRandomNumber is impure as it returns different outputs for the same input.
  • doubler2 is a partial function because it is not defined for all the inputs. If we pass in 5, it won’t return any value. Let’s assume the only inputs to the function can be 1, 2 or 3, in that case,
    1. It is a Total function, as now it is defined for all its inputs.
    2. It is also a Pure function, as it will return the same output for the same input.
  • increment is a total function but not pure. It has a side effect as it updates an external state (i.e. counter).

totalDivide is a total function. By default, Dart performs division by double. When dividing by zero, it won’t throw an exception and instead return a double.infinity, a constant in Dart. Hence, as the function is defined for all the possible inputs, it is a total function. However, the ideal solution in such cases should be to use a Monad like Option or Either which we have not covered yet.

partialDivide is a partial function. For using integer division, we need to use the ~/ operator. This will perform integer division and on passing zero, will throw an exception. As the function is not defined for all the possible inputs, which in our case is zero, it is a partial function.

Now, all this looks good in theory and with such simple examples, but what are the benefits one get and how to implement this in a practical way?

What are the benefits?

  • Pure functions are easy to unit test and can test in any order. We do not need to set up and tear down as they do not depend on any global state. Dependencies can be mocked and injected easily.
  • As they do not depend on external states, the chances of unexpected bugs reduce. We do not have to worry about making changes in module A, and somewhere something breaks in a completely different module H.
  • Pure functions provide caching mechanisms that help in reducing heavy computation, API calls etc. (We are going to cover this in-depth in our Memoization Article)
  • Pure functions are easy to Parallelize as they are deterministic, and the output only depends on the input.

Referential Transparency

Another way to determine if a function is pure is that they are referentially transparent.

Referential transparency means a function call can be replaced by its return value and not affect the rest of the program.

The function square is referentially transparent. We can replace the function call, let’s say square(2), directly by its value i.e. 4, and it won’t affect the program. Compilers in pure FP languages like Haskell takes advantage of this which increases the performance.

How to implement in real-world applications?

When working with these concepts, the motive is to keep the computation and business logic pure and free from side effects. All of the API calls, database transactions and stuff with side effects move towards the external layer of the application. Our domain, which consists of business logic, is free from all these side effects, and we can rely on it.

Awesome! Pat yourself on the back for reaching till the end. I hope I added some value to the time you invested. Find out more examples on the GitHub repository, and reach out on Twitter or LinkedIn for suggestions/Questions or any topic you’d like me to cover. You can support it by clapping👏, and thanks for reading :) Follow for more😄

Until next time, folks!

Other Articles in this Series

--

--

Yogesh Parwani
Yogesh Parwani

Responses (1)