Why are FP devs obsessed with Referential Transparency?
Tags: functional, haskell, scala, rust
Published: 2023-02-28 17:32:41

📹 Hate reading articles? Check out the complementary video, which covers the same content: https://youtu.be/UsaduCPLiKc

---

I want to clarify referential transparency (RT), why it is so cool, what it has to do with side-effects, and what common misconceptions exist. 

For instance, how can the code have no “side-effects”, but the program can print to the console?

---

💡 Note that this concept goes beyond functional programming and even programming in general.

---

## What is referential transparency?

Let’s start on a bit of a tangent. Let’s talk about **pure functions**. A pure function computes a result given its inputs and has no other observable (after)effects on the execution of the program.

Let’s look at a pseudo-code example, **pure function:**

```
Function's type: Take an Int, return an Int
Function's body: Take an Int, use it, return another Int
```

*It doesn’t do anything else.*

On the contrary, **impure function:**

```
Function's type: Take an Int, return an Int
Function's body: Go to the database, call another service, return a random Int
```

*In this case, the function signature is basically a lie.* 

So, pure functions allow us to know precisely what a function does by looking at its signature. The cool thing about it is not that it’s so “mathematical”; the cool thing is this property it has. And there is even more to this property!

It’s called referential transparency, and it’s not tied to functions: we can apply it to programs, expressions, etc.

For instance, a **referentially transparent** expression can be replaced with its value without changing the program's behavior and vice versa. So, the following snippets should be the same:

```rust
let a = <expr>
(a, a)
```

```rust
(<expr>, <expr>)
```

*If we have some variable `a`, we can substitute it with the actual expression, and the program should stay the same.*

Imagine that we want to calculate a simple mathematical expression:

```rust
let a = 3
(a + 1) + (a * 4) // 16
```

We can replace `a` with `3`:

```rust
(3 + 1) + (3 * 4) // 16
```

And the result stays the same. We can do the same exercise with strings:

```rust
let a = "Three"
a ++ a // "ThreeThree"
```

Where the `++` operator is a string concatenation.

```rust
"Three" ++ "Three" // "ThreeThree"
```

Before we move to more complex examples, which include printing text to the standard output, let’s see why we should even bother.

### Why should I care?

Referential transparency improves the developer's quality of life and allows program optimizations.

The property guarantees that the code is context-independent, which enables local reasoning and code reusability. It means that you can take a piece of code and understand it better – you can actually reason about it without worrying about the rest of the program or the state of the world.

Suppose we have an `equals` function that compares two URLs for equality:

```
equals: take two URLs, return a boolean
```

Looks quite innocent. But! The problem is that this is a Java function, and there is no such thing as referential transparency in Java. So when you use this function with no internet connection, it doesn’t work.

![impure function works depending on your network status](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5iepce6vywylijarr0xr.png)

I repeat, a function (technically, a method) that should simply compare two objects and return a boolean either works or doesn’t, depending on your network status.

---

💡 If you're wondering. From the [docs](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/URL.html#equals(java.lang.Object)): "Two hosts are considered equivalent if both hostnames can be resolved into the same IP addresses". So the [`equals`](https://bugs.java.com/bugdatabase/view_bug.do?bug_id=4434494) performs DNS resolution.

---

*I love this example, but we should move on.*

If the code is context-independent – it’s deterministic: we can refactor it without pain and write tests for it because all the inputs can be passed as arguments – we don’t need to mock or integrate anything. The behavior is explicit and expected.

And because it’s deterministic, it can be optimized: computations can be cached and parallelized because the outputs are defined by the inputs and don’t implicitly interact. Also, the compiler or runtime can execute the program in whichever order.

We get a more maintainable code with additional optimization opportunities thanks to referential transparency.

### What are side-effects?

Since you’re still here, I’ll let you in on a secret. Functional programmers are not some kind of monks writing programs on paper: we print stuff, talk to databases, etc. We just use a different definition of the term “side-effect”.

In “pure” functional programming, **side-effects** are things that break referential transparency. When we talk about a program without side-effects, we mean programs without breaking or violating referential transparency.

And if we want to refer to things like I/O (input/output) or talking to a database, we use the term “computational effects” or “effects”. 

*Well, sometimes we also say “side-effects” because natural languages are also complicated, so it can all be confusing and ambiguous without context and experience.*

## Referential Transparency in the wild

### Referential Transparency and Rust

Okay, let’s make some chaos and print some nonsense!

We’ll start with a Rust example, but it applies to any language without referential transparency guarantees: Java, JavaScript, Python, and what have you.

You must keep an eye on your code – the code might not behave as expected, and refactoring might break the logic! Let’s test it:

```rust
let a: i32 = {
    println!("the toast is done");
    3
};

let result = (a + 1) + (a * 4);

println!("Result is {result}");

// Prints:
// the toast is done
// Result is 16
```

If we inline `a`, first of all, it looks a bit ugly, but more importantly, we get different results:

```rust
let result = ({
    println!("the toast is done"); 3} + 1)
    + ({ println!("the toast is done"); 3} * 4);

println!("Result is {result}");

// Prints:
// the toast is done
// the toast is done
// Result is 16
```

*The second version prints that the toast is done twice.*

In most languages, we can perform arbitrary side effects anywhere and anytime, so don’t expect any referential transparency.

### Referential Transparency and Scala

But some languages, such as Scala, give us more control over referential transparency.

We can do the same exercise we did with Rust, but we can also go one step further and write some async code.

Imagine we have two functions with some arbitrary side-effects:

```scala
def computeA(): Int = { println("Open up a package of My Bologna"); 1 }
def computeB(): Int = { println("Ooh, I think the toast is done"); 2 }
```

<aside>
📹 The first function does some printing and returns `1`.
The second one does some other printing and returns `2`.

</aside>

`Future` represents a result of an asynchronous computation, which may become available at some point. When we create a new `Future`, Scala starts an asynchronous computation and returns a future holding the result of that computation.

So let’s spawn our computations:

```scala
import scala.concurrent.Future

val fa = Future(computeA())
val fb = Future(computeB())

for {
  a <- fa
  b <- fb
} yield a + b

// Probably prints:
// Open up a package of My Bologna
// Ooh, I think the toast is done

// or prints:
// Ooh, I think the toast is done
// Open up a package of My Bologna
```

---

💡 Note that we must provide an implicit `ExecutionContext` to run this code.

For example, by importing the global one:

```scala
import concurrent.ExecutionContext.Implicits.global
```

---

Async execution is unpredictable. By looking at this snippet, we, as developers, shouldn’t expect any guarantees about execution order: we can see the effects of the `computeA` first or from the `computeB`. It is up to the thread pools and compilers to decide. And it’s okay; that’s the whole premise of asynchronous programming.

What is not okay is that if we try refactoring this code by inlining the variables, we get a different program:

```scala
for {
  a <- Future(computeA())
  b <- Future(computeB())
} yield a + b

// Will print:
// Open up a package of My Bologna
// Ooh, I think the toast is done
```

This one is sequential. Why? Because it’s not referentially transparent.

Luckily, multiple alternatives guarantee RT; one of them is [`cats-effect`](https://typelevel.org/cats-effect/) [`IO`](https://typelevel.org/cats-effect/api/3.x/cats/effect/IO.html). 

---

💡 Note that `IO` isn’t the same as I/O.

---

> A value of type `IO[A]` is a computation that, when evaluated, can perform effects before returning a value of type `A`.
> 

`IO` data structure is similar to `Future`, but its values are pure, immutable, and preserve referential transparency.

The following programs are equivalent:

```scala
import cats.effect.IO

val fa = IO(computeA())
val fb = IO(computeB())

for {
  a <- fa
  b <- fb
} yield a + b
```

```scala
for {
  a <- IO(computeA())
  b <- IO(computeB())
} yield a + b
```

The computations will run sequentially – first, `a` and then `b`.

---

💡 If we want to run them in parallel, we have to be explicit:

```scala
import cats.implicits._

(IO(computeA()), IO(computeB())).parMapN(_ + _)
```

---

So what is happening here? How is `IO` referentially transparent? Let’s switch to Haskell and debunk this datatype.

### Referential Transparency and Haskell

Haskell is pure – invoking any function with the same arguments always returns the same result. 

Haskell separates ”expression evaluation” from “action execution”.

**Expression evaluation** is the world where pure functions live and which is always referentially transparent.

**Action execution** is not referentially transparent.

Haskell also has an `IO` datatype. As I’ve mentioned before, `IO a` is a computation: when **executed**, it can perform arbitrary effects before returning a value of type `a`. But here comes the essential point. **Executing** `IO` is not the same as **evaluating** it. Evaluating an `IO` expression is pure.

For instance, here is the type signature of `print`:

```haskell
print :: Show a => a -> IO ()
```

This function returns a pure value of type `IO ()` (unit) – a value like any other: we can pass them around, store them in collections, etc.

Okay, we have an `IO` function. How do we run it? We need to define the `main` `IO` function of the program – the program entry point – which will be **executed** by the Haskell runtime. Let’s look at the executable module example: it asks the user for the name, reads it, and prints the greeting.

```haskell
module Main where 

greet :: String -> IO ()
greet name = print $ "Hello, " <> name

main :: IO ()
main = do
  print "What's your name?"
  name <- getLine
  greet name
```

<aside>
📹 If we run the program and pass it the name Ali, it will print back the greeting “Hello, Ali.”

</aside>

This differs from all the languages in which we can perform side effects anywhere and anytime. We can’t print outside of `IO`. We get a compilation error if we try otherwise:

```haskell
noGreet :: String -> String
noGreet name = print $ "Hello, " <> name
--             ^^^^^^^^^^^^^^^^^^^^^^^^^
-- Couldn't match type: IO ()
--                with: String
```

### Referential Transparency and Analytical Philosophy

Some trivia before we wrap up: referential transparency has its roots in analytical philosophy.

The "referent", the thing that an expression refers to, can substitute the "referrer" without changing the meaning of the expression.

For example, let’s take the statement:

> "The author of *My Bologna* is best known for creating comedy songs."
> 

"The author of *My Bologna*" references "Weird Al Yankovic". This statement is referentially transparent because "The author of *My Bologna*" can be replaced with "Weird Al Yankovic". The message will have the same meaning.

But the following statement is not referentially transparent:

> "My Bologna is Weird Al Yankovic's first song.”
> 

We can't do such a replacement because it produces a sentence with an entirely different meaning: "My Bologna is the author of My Bologna's first song".

## Conclusion

In conclusion, a side effect is a lack of referential transparency. Referential transparency allows us to trust the functions and reason about the code. It gives us the following:

- local reasoning;
- smaller debugging overload;
- maintainable code;
- explicit and expected behavior;
- less painful refactoring.

This property is a crucial advantage and source of many good things about “pure” functional programming.

---

🖼️ If you want some recaps or cheat sheets, checkout these pictures:

- [What is Referential Transparency](https://impurepics.com/posts/2018-04-01-referential-transparency.html)
- [Referential Transparency in the Wild](https://impurepics.com/posts/2018-07-13-referential-transparency-wild.html)