19

In programming, what are the benefits of referential transparency?

RT makes one of the major differences between functional and imperative paradigms, and is often used by advocates of the functional paradigm as a clear advantage over the imperative one; but in all of their efforts, these advocates never explain why it is a benefit to me as a programmer.

Sure, they'll have their academic explanations to how "pure" and "elegant" it is, but how does it make it better than a less "pure" code? How does it benefit me in my day-to-day programming?

Note: This is not a duplicate of What is referential transparency? The latter addresses the topic of what is RT, while this question adressses its benefits (which may not be so intuitive).

10
  • Related: stackoverflow.com/questions/210835/…
    – meriton
    Nov 13, 2016 at 19:18
  • 2
  • 3
    Referential transparecy allows you to use equational reasoning to: 1) Prove properties of the code and 2) write programs. There are a few books about Haskell where the authors should how you can start from some equations that you want a function to fullfil and using just equational reasoning you end up obtain an implementation of said function, which is, hence, certainly correct. Now how much this can be applied in "day-to-day" programming probably depends on the context...
    – Bakuriu
    Nov 13, 2016 at 20:07
  • 2
    @err Do you like code that's easier to refactor because you know whether calling a function twice is the same as storing its value in a variable, then using said variable twice? Would you say that's a benefit for your day to day programming?
    – Andres F.
    Nov 13, 2016 at 21:22
  • The benefit is you don't need to waste time thinking about referencial nontransparency. Kinda like how the benefits of variables are that you don't need to waste time thinking about register allocation.
    – user253751
    Nov 13, 2016 at 23:26

4 Answers 4

37

The benefit is that pure functions make your code easier to reason about. Or, in another words, side effects increase the complexity of your code.

Take an example of computeProductPrice method.

A pure method would ask you for a product quantity, a currency, etc. You know that whenever the method is called with the same arguments, it will always produce the same result.

  • You can even cache it and use the cached version.
  • You can make it lazy and postpone its call to when you actually need it , knowing that the value won't change meanwhile.
  • You can call the method multiple times, knowing that it won't have side effects.
  • You can reason about the method itself in an isolation from the world, knowing that all it needs are the arguments.

A non-pure method will be more complex to use and debug. Since it depends on the state of the variables other than the arguments and possibly altering them, it means that it could produce different results when called multiple times, or not have the same behavior when not called at all or called too soon or too late.

Example

Imagine there is a method in the framework which parses a number:

decimal math.parse(string t)

It doesn't have referential transparency, because it depends on:

  • The environment variable which specifies the numbering system, that is Base 10 or something else.

  • The variable within the math library which specifies the precision of numbers to parse. So with the value of 1, parsing the string "12.3456" will give 12.3.

  • The culture, which defines the expected formatting. For instance, with fr-FR, parsing "12.345" will give 12345, because the separation character should be ,, not .

Imagine how easy or difficult would it be to work with such method. With the same input, you can have radically different results depending on the moment when you call the method, because something, somewhere changed the environment variable or switched the culture or set a different precision. The non-deterministic character of the method would lead to more bugs and more debugging nightmare. Calling math.parse("12345") and obtaining 5349 as an answer since some parallel code was parsing octal numbers isn't nice.

How to fix this obviously broken method? By introducing referential transparency. In other words, by getting rid of global state, and moving everything to the parameters of the method:

decimal math.parse(string t, base=10, precision=20, culture=cultures.en_us)

Now that the method is pure, you know that no matter when you call the method, it will always produce the same result for the same arguments.

11
  • 4
    Just an addendum: referential transparency applies to all expressions in a language, not just functions.
    – gardenhead
    Nov 13, 2016 at 18:05
  • 3
    Note that there are limitations on how transparent you can be. Making packet = socket.recv() referentially transparent rather defeats the point of the function.
    – Mark
    Nov 13, 2016 at 19:51
  • 1
    Should be culture=cultures.invariant. Unless you want to accidentally create software that only works properly in the US.
    – user253751
    Nov 13, 2016 at 23:28
  • @immibis: hm, good question. What would be the parsing rules for invariant? Either they are the same as for en_us, in which case, why bother, or they correspond to some other country, in which case, which one and why this one instead of en_us, or they have their specific rules which don't match any country anyway, which would be useless. There is really no “true answer” between 12,345.67 and 12 345,67: any “default rules” will work for a few countries, and won't work for others. Nov 13, 2016 at 23:50
  • 3
    @ArseniMourzenko It's generally a "lowest common denominator" and similar to the syntax many programming languages use (which is also culture-invariant). 12345 parses as 12345, 12 345 or 12,345 or 12.345 is an error. 12.345 parsed as an invariant floating-point number always yields 12.345, in accordance with the programming language convention of using . as the decimal separator. Strings are sorted by their Unicode code points and case-sensitively. And so on.
    – user253751
    Nov 14, 2016 at 0:08
12

Do you often add a break point to a point in your code and run the app in the debugger in order to work out what's happening? If you do, that's largely because you aren't using referential transparency (RT) in your designs. And so have to run the code to work out what it does.

The whole point to to RT is that the code is highly deterministic, ie you can read the code and work out what it does, every time, for the same set of inputs. Once you start adding in mutating variables, some of which have scope beyond a single function, you can't just read the code. Such code has to be executed, either in your head or in the debugger, to work out how it truly works.

The simpler the code is to read and reason, the simpler it is to maintain and to spot bugs, so it saves time and money for you and your employer.

4
  • 10
    "Once you start adding in mutating variables, some of which have beyond a single function, you can't just read the code, you have to execute it, either in your head or in the debugger, to work out how it truly works.": Good point. In other words, referential transparency does not only mean that a piece of code will always produce the same result for the same inputs, but also that the produced result is the only effect of that piece of code, that there are no other, hidden side effect like changing some variable that has been defined far away in another module.
    – Giorgio
    Nov 13, 2016 at 19:23
  • That is a good point. I do have a bit of a problem with the the simpler the code is to read/reason argument, since simpler to read or reason is a somewhat vague and subjective attribute of code.
    – Eyal Roth
    Nov 14, 2016 at 15:41
  • Once you start adding in mutating variables, some of which have scope beyond a single function but why assignment operation is discouraged even when variable scope is local to function ? Nov 28, 2018 at 22:13
  • 1
    Being able to "just read the code" is subjective, yes. For example tail-recursive code, or code using reduce (aka fold), are loops in disguise which — while technically pure and RT — are equivalent to re-assigning values from iteration to iteration and may require "executing, either in your head or debugger" to work out what they do. Still, this is a good characterization! The goal of code being readable without "executing step-by-step" is a quality RT helps chasing / non-RT undermines. Sep 19, 2023 at 9:06
9

People throw around the term "easier to reason about," but never explain what that means. Consider the following example:

result1 = foo("bar", 12)
// 100 lines of code
result2 = foo("bar", 12)

Are result1 and result2 the same or different? Without referential transparency, you have no idea. You have to actually read the body of foo to make sure, and possibly the body of any functions foo calls, and so forth.

People don't notice this burden because they are accustomed to it, but if you go work in a purely functional environment for a month or two then come back you will feel it, and it is a huge deal.

There are so many defense mechanisms people do to work around the lack of referential transparency. For my little example, I might want to keep result1 around in memory, because I wouldn't know if it would change. Then I have code with two states: before result1 was stored and after. With referential transparency, I can just recalculate it easily, as long as the recalculation is not time consuming.

3
  • 1
    You mentioned that referential transparency allows you to reason about the result of the calls to foo() and know whether result1 and result2 are the same. Another important aspect is that if foo("bar", 12) is referentially transparent, then you do not have to ask yourself whether this call has produced some effects somewhere else (set some variables? deleted a file? whatever).
    – Giorgio
    Nov 13, 2016 at 19:42
  • The only "referential integrity" I'm familiar with involves relational databases.
    – Mark
    Nov 13, 2016 at 19:56
  • 1
    @Mark It's a typo. Karl meant referential transparency, as is obvious from the rest of his answer.
    – Andres F.
    Nov 13, 2016 at 21:18
6

I'd say: referential transparency isn't only good for functional programming but for everyone who works with functions because it follows to principle of least astonishment.

You have a function and can reason better about what it does because there are no external factors you need to take in account, for a given input the output will always be the same. Even in my imperative language I try to follow this paradigm as much as possible, the next thing that basically automatically follows from this is: small easy to understand functions instead of the gruesome 1000+ line functions I sometimes run in.

Those large functions do magic and I'm afraid to touch them because they can break in spectacular ways.

So pure functions aren't something only for functional programming, but for every program.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.