Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

pure function in rxjs

I am learning rxjs and am in the process of converting some of my old-school array manipulation functions to use rxjs. Here is one that groups items in an array:

interface FilmGroup {
    key: string;
    films: Film[]
}

private groupFilms(items: Film[]): Observable<FilmGroup[]> {

    return from(items)   
    .pipe(
        groupBy(item => item.name),

        mergeMap(group$ => group$
            .pipe(
                toArray(),
                map(films => ({ key: group$.key, films})  <==== Not a pure function!
            )
        )),
        
        toArray()
    );
}

When subscribed to this function produces a result of the form

[
  {
    "key": "a",
    "films": [
      {
        "id": 1,
        "name": "a"
      },
      {
        "id": 2,
        "name": "a"
      }
    ]
  },
  {
    "key": "b",
    "films": [
      {
        "id": 3,
        "name": "b"
      }
    ]
  },
  etc...
]

This works as expected, but I am concerned about the way the function is written. The rxjs book I use strongly advises only using pure functions when writing rxjs code. The projection for map( films => ...) is not a pure function since it pulls in the group$.key value from an outer scope, not via the parameters.

Is this an OK thing to do, or am I approaching this the wrong way?

like image 860
Paul D Avatar asked Oct 26 '25 06:10

Paul D


1 Answers

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 is 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 have 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 is 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 intro into theory and its application was needed because now we need to think about your case: Is the following

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 g = 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 would not 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 is 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 is now dissociates from its context.

like image 134
VLAZ Avatar answered Oct 28 '25 22:10

VLAZ



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!