Home > Back-end >  How is injecting an impure function different from calling it?
How is injecting an impure function different from calling it?

Time:09-27

I am reading a book where it says one way to handle impure functions is to inject them into the function instead of calling it like the example below.

normal function call:

const getRandomFileName = (fileExtension = "") => {
  ...
  for (let i = 0; i < NAME_LENGTH; i  ) {
    namePart[i] = getRandomLetter();
  }
  ...
};

inject and then function call:

const getRandomFileName2 = (fileExtension = "", randomLetterFunc = getRandomLetter) => {
  const NAME_LENGTH = 12;
  let namePart = new Array(NAME_LENGTH);
  for (let i = 0; i < NAME_LENGTH; i  ) {
    namePart[i] = randomLetterFunc();
  }
  return namePart.join("")   fileExtension;
};

The author says such injections could be helpful when we are trying to test the function, as we can pass a function we know the result of, to the original function to get a more predictable solution.

Is there any difference between the above two functions in terms of being pure as I understand the second function is still impure even after getting injected?

CodePudding user response:

An impure function is just a function that contains one or more side effects that are not disenable from the given inputs.

That is if it mutates data outside of its scope and does not predictably produce the same output for the same input.

In the first example NAME_LENGTH is defined outside the scope of the function - so if that value changes the behaviour of getRandomFileName also changes - even if we supply the same fileExtension each time. Likewise, getRandomLetter is defined outside the scope - and almost certainly produces random output - so would be inherently impure.

In second example everything is referenced in the scope of the function or is passed to it or defined in it. This means that it could be pure - but isn't necessarily. Again this is because some functions are inherently impure - so it would depend on how randomLetterFunc is defined.

If we called it with

getRandomFileName2('test', () => 'a');

...then it would be pure - because every time we called it we would get the same result.

On the other hand if we called it with

getRandomFileName2(
  'test', 
  () => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.charAt(Math.floor(25 * Math.random()))
);

It would be impure, because calling it each time would give a different result.

CodePudding user response:

There's more than one thing at stake here. At one level, as Fraser's answer explains, assuming that getRandomLetter is impure (by being nondeterministic), then getRandomFileName also is.

At least, by making getRandomFileName2 a higher-order function, you at least give it the opportunity to be a pure function. Assuming that getRandomFileName2 performs no other impure action, if you pass it a pure function, it will, itself, transitively, be pure.

If you pass it an impure function, it will also, transitively, be impure.

Giving a function an opportunity to be pure can be useful for testing, but doesn't imply that the design is functional. You can also use dependency injection and Test Doubles to make objects deterministic, but that doesn't make them functional.

In most languages, including JavaScript, you can't get any guarantees from functions as first-class values. A function of a particular 'shape' (type) can be pure or impure, and you can check this neither at compile time nor at run time.

In Haskell, on the other hand, you can explicitly declare whether a function is pure or impure. In order to even have the option of calling an impure action, a function must itself be declared impure.

Thus, the opportunity to be impure must be declared at compile time. Even if you pass a pure 'implementation' of an impure type, the receiving, higher-order function still looks impure.

While something like described in the OP would be technically possible in Haskell, it would make everything impure, so it wouldn't be the way you go about it.

What you do instead depends on circumstances and requirements. In the OP, it looks as though you need exactly 12 random values. Instead of passing an impure action as an argument, you might instead generate 12 random values in the 'impure shell' of the program, and pass those values to a function that can then remain pure.

There's more at stake than just testing. While testability is nice, the design suggested in the OP will most certainly be impure 'in production' (i.e. when composed with a proper random value generator).

Impure actions are harder to understand, and their interactions can be surprising. Pure functions, on the other hand, are referentially transparent, and referential transparency fits in your head.

It'd be a good idea to have as a goal pure functions whenever possible. The proposed getRandomFileName2 is unlikely to be pure when composed with a 'real' random value generator, so a more functional design is warranted.

CodePudding user response:

Anything that contains random (or Date or stuff like that). Will be considered impure and hard to test because what it returns doesn't strictly depends on its inputs (always different). However, if the random part of the function is injected, the function can be made "pure" in the test suite by replacing whatever injected randomness with something predictable.

function getRandomFileName(fileExtension = "", randomLetterFunc = getRandomLetter) {}

can be tested by calling it with a predictable "getLetter" function instead of a random one:

getRandomFileName("", predictableLetterFunc)
  • Related