Referential Transparency

I usually don't wear a watch, but recently I found a watch I got as a gift at the bottom of my closet. I dusted it off, went to a watch repair shop to change the battery, and it's ticking again. Wearing it for a few days, I noticed a really good advantage of wearing a watch: you can tell the time! Right now you are probably thinking "Are you kidding me?" and I'm not! By this point I'm used to pulling out my phone and waiting for it to wake up to tell me what time it is. It actually shocked me how cool it is to just glimpse at your wrist and instantly know that it's 9:15 AM.

I realized another thing about watches. Every time you look at them, it shows a different time. At this point your eyes are probably rolling so hard that they're about to pop out of your eye sockets, but this very obvious fact is the crux of what we're talking about today. Watches are useful precisely because they time is constantly changing, and whenever you look at them they tell you the current time.

Because the watch changes every time you look at it, we can say that a watch is referentially opaque. In programming, referentially opaque functions are those that provide a different result depending on when they are called. In programming, a watch would be a function which tells you the current time. If you call that function twice, you will get two different results.

a = currentTime()
b = currentTime()
print(a == b) // false

This is contrasted by referentially transparent functions. When a function is referentially transparent, it can be called at any time and anywhere and will always give the same result for the same input.

def times(x, y) = x * y

a = times(3, 2)
b = times(3, 2)
print(a == b) // true

Before I changed the battery, the watch was referentially transparent. The watch would always say it's half past 12. At that point, I didn't need to look at it to know what it was saying. I knew it will always be lunch time as far as the watch is concerned.

This is also true in programming. If a function is referentially transparent, you only need to call it once, and you know that every future result will be the same. You can freely replace times(3, 2) with 6, and the program will work just as well.

This is the one defining characteristic of referentially transparent functions: you can replace a function call with its result, and your program will still work.

This has a lot of cool consequences. For one thing, it's much easier to reason about your code. You can think of referentially opaque functions as functions that depend on time. This means that you have one more variable to keep track of, and a whole new dimension to think about. This adds to your cognitive strain while you're coding. It's easier to draw a triangle than a pyramid, right?

Referentially transparent functions are also easier for the compiler to grasp. If the compiler knows that a function will always give the same result for the same input, it can make some shortcuts. Just like I didn't need to look at my non-working watch, the compiler doesn't need to call transparent functions more than once.

You can call times(3, 2), and just remember that it's 6. The next time it has to do the same thing, it can just remember that it was 6, without doing any calculation. This is called memoization, and it's a common optimization technique in a lot of languages. The compiler can essentially make multiplication tables, but for all functions. This can greatly improve the speed of evaluating complex functions.

Another very useful trait of referential transparency is that you can completely reorder the function calls, and the program will still work the same. If times(3, 2) is always 6, then it will be 6 at any time. This means you can take 500 calls to the times function and do them all in parallel, and the program will still work. Or you can take a slow function and call it first while you evaluate the rest of the code, improving the overall performance.

All of these benefits are why a lot of programming languages embrace referential transparency. In Haskell, all functions are referentially transparent, and this makes things like lazy evaluation easy to manage. But you don't need to be a Haskell programmer to get all these benefits.

All languages can write referentially transparent functions, and you should strive to write as many of them as you can. Next time you're writing a function, ask yourself "If I call this twice, will I get the same result?". If not, try to rewrite it to make the function's dependencies more explicit. If it depends on the current time or some other global variable, just pass it as a parameter.

You will thank your past self when you are reading through the code and have a much easier time understanding what's happening.

References:

Referential transparency - Wikipedia
https://en.wikipedia.org/wiki/Referential_transparency

A great Stack Overflow answer with some history background
https://stackoverflow.com/questions/210835/what-is-referential-transparency/9859966#9859966

Dependent Type

Cyclomatic Complexity