Let's look at some theory first.
Here is a curious property of functions - an impure function that uses some outside data can be converted to a pure function by making the data a parameter.
Let's start with an impure function:
const getSomeData = () =>
Math.random() < 0.5
? "foo"
: "hello"
const fn = thing =>
`${getSomeData()} ${thing}`;
console.log(fn("world"));
console.log(fn("world"));
console.log(fn("world"));
console.log(fn("world"));
Right now fn
depends on someData
that we cannot predict. It's impure.
Repeat application will not yield the same result.
If we fold the data as a parameter, we can achieve repeatable and pure results:
const getSomeData = () =>
Math.random() < 0.5
? "foo"
: "hello"
const fn = (prefix, thing) =>
`${prefix} ${thing}`;
const data = getSomeData();
console.log(fn(data, "world"));
console.log(fn(data, "world"));
console.log(fn(data, "world"));
console.log(fn(data, "world"));
Any call to fn
with the same parameters now yields the same results.
However, we've changed the arity of the function and made it binary (takes two arguments, rather than works with base 2 numbers). It makes using it awkward as a unary function is more useful in some cases like
const arr = ["Alice", "Bob", "Carol"];
const fn = (prefix, thing) =>
`${prefix} ${thing}`;
console.log(
arr.map(name => fn("hello", name))
)
It's usable but awkward, as we still need another function on top of it.
Here is where a useful tool called currying comes in - any function with multiple arguments can be converted to a series of unary functions:
const arr = ["Alice", "Bob", "Carol"];
const fn = prefix => thing =>
`${prefix} ${thing}`;
console.log(
arr.map(fn("hello"))
)
The this curried version of fn
is still pure because repeat calls with the same parameters still yield the same result. You can even capture each application in a variable like const g = fn("hello")
and call g("world")
which is identical to fn("hello")("world")
. As can be seen above, this is handy when passing functions to something like .map()
when you need multiple arguments but only the last one would vary.
This quick into into theory and it's application was needed because now we need to think about your case - Is
group$ => group$
.pipe(
toArray(),
map(films => ({ key: group$.key, films})
)
really dissimilar to what we did with the curried function above? Currying works because the inner function have access to variables outside of themselves. However, this is not going to be impure, because the outer parameters are not going to change. So, each inner function is still pure because it maintains its referential transparency - a function call can be replaced with its result. Let's revisit:
const a = fn("hello")
g("world") === fn("hello")("world")
//^ ^^^^^^^^^^^
//these two can be freely substituted to one another
So, with this context in mind films => ({ key: group$.key, films})
can still be considered pure, since group$.key
is never going to change. Repeat executions of this function will yield the exact same results again. Therefore, I wouldn't personally worry about it.
Still, just to round things off, this can be abstracted away as a pure curried function:
const makeFilmObj = key => films =>
({ key, films });
/* ... */
mergeMap(group$ => group$
.pipe(
toArray(),
map(makeFilmObj(group$.key))
)),
in this case, it's valid refactoring but it seems like a bit excessive. This might be a good approach if the makeFilmObj
was perhaps to be reused later and/or perhaps unit tested as it's now dissociates from its context.