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).

Eyal Roth
  • 623

4 Answers4

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.

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.

David Arno
  • 39,599
  • 9
  • 94
  • 129
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.

Karl Bielefeldt
  • 148,830
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.

Pieter B
  • 13,310