Global environment variables violate a core principle of functional programming. For example, this is not very acceptable in the FP world:
def hello =
if(locale == 'en.US')
"hello"
elsif(locale == 'fr.FR')
"bonjour"
locale
shouldn’t just be there as a global. In an pure functional language it should be passed as a parameter, according to the the principle of referential transparency.
def hello(locale) =
if(locale == 'en.US')
"hello"
elsif(locale == 'fr.FR')
"bonjour"
But this means you also have to pass locale
to every function that calls hello
.
def greet(name, locale) =
hello(locale) ++ ", " ++ name
Which means you have to pass locale
to every function that calls greet
, and so on. Turtles all the way down.
Given/Inject
Preprocessing
In my post on functional dependency injection and inversion of control, I discuss a solution for passing dependencies without this syntactic overhead, using given/inject
statements and code pre-processing.
We usually think of “dependencies” as libraries or services. But anything your code depends on can be thought of as a dependency. In any case, we can use given
to thread locale
through our code without the semantic overhead.
given locale
def greet(name) =
hello() ++ ", " ++ name
def hello() =
if(locale == 'en.US')
"hello"
elsif(locale == 'fr.FR')
"bonjour"
The given
statement causes:
- `greet` and `hello` to be rewritten in the pre-processing stage to accept an invisible `locale` parameter
- `greet` to pass the `locale` parameter in its call to `hello`.
Now other functions can inject
a value for locale
. The inject
statement causes locale to be passed as a parameter to every subsequent function call in the code block that needs it.
def main(environment) =
inject locale = environment.locale
greet("Everybody")
Now we have the convenience of what kind of looks like a global variable. But it is implemented in such a way that the core principles of FP – no side-effects and referential transparency – are respected.
Referential Transparency
You might argue that, because you don’t actually see these parameters literally being passed in your code, given/inject
violates referential transparency.
But the referential transparency rule simply requires that an expression can be replaced with its value without changing the output of the program. The value of an expression involving a given
simply depends on that variable being bound to a value. Once bound, the expression can be evaluated, and it will be interchangeable with its value.
Referential transparency is not about syntax: it’s about having well-defined, reproducible behavior that is determined entirely by inputs. There is nothing non-FP about using macros and pre-processors to save some keystrokes.
Clarity
Some programmers might fear that bugs and confusion may arise when programmers don’t realize that their program behavior depends on an environment variable. But if even indirect dependencies on environment variables must be declared with given
statements, it should be just as clear to a programmer reading the code what is going on.
Preprocessing vs Monads
Another purely functional approach to environment variables is the use of the Reader Monad. But this requires a great deal more syntactic overhead than given/inject
. The signature of every function must be modified to return a Reader, and every function call involves a bind. Code pre-processing with given/inject does this work for you.
Conclusion
given/inject
preprocessing lets you use environment variables in a purely functional way without the work and complexity of threading them as parameters throughout your code.