George Powell
George Powell

Reputation: 9499

What is the difference between Observable.lift and Observable.pipe in rxjs?

The docs define Observable.lift(operator: Operator) as:

Creates a new Observable, with this Observable as the source, and the passed operator defined as the new observable's operator.

and Observable.pipe(operations: ...*) as:

Used to stitch together functional operators into a chain. Returns the Observable result of all of the operators having been called in the order they were passed in.

So clearly .pipe can accept multiple operators, which .lift cannot. But pipe can also accept a single operator, so this cannot be the only difference. From the docs alone it isn't clear to me what they are both for and why they exist. Can someone please explain the purpose of each of these functions, and when each of them should be used?


Observations so far

The following code (typescript):

let myObservable = Observable.of(1, 2, 3);
let timesByTwoPiped = myObservable.pipe(map(n => n * 2));
let timesByTwoLift = myObservable.lift(new TimesByTwoOperator());

timesByTwoPiped.subscribe(a => console.log('pipe:' + a));
timesByTwoLift.subscribe(a => console.log('lift:' + a));

and TimesByTwoOperator:

class TimesByTwoOperator implements Operator<number, number> {
  call(subscriber: Subscriber<number>, source: Observable<number>): void | Function | AnonymousSubscription {
    source.subscribe(n => {
      subscriber.next(n * 2);
    });
  }
}

Seems to achieve the same result using both .lift and .pipe. This experiment shows I'm correct in thinking that both lift and pipe can be used to achieve the same thing, albeit with the pipe version being more succinct in this case.

As the Operator type that is passed in to .lift is given full access to the source observable and subscriptions, clearly powerful things could be achieved with it; for example keeping state. But I'm aware that the same sort of power can also be achieved with .pipe, for example with the buffer operator.

It's still not clear to me why they both exist and what each is designed for.

Upvotes: 22

Views: 18610

Answers (3)

Artsiom Kukharev
Artsiom Kukharev

Reputation: 11

ADT(algebraic data types - alternative to OOP classes)

Lifting is often used together with Monads. RXJS library itself - represents Monad(and bunch of others) ADT.

"lift" comes from FP and ADT. Here is the descriptive explanation what term "lift" means: https://wiki.haskell.org/Lifting

TLDR:

const sum = a => b => a + b;

console.log(sum(2)(3)); // 5

// and now we need to reuse our "sum" within a Monad level.
// it means we need to "lift" our "sum" function to the Monad level:

// assumption: our Monad - is just an Array


const monad0 = [2];
const monad1 = [3];

// lift1 === fmap
// lift1:: Monad M: (a -> b) -> M a -> M b
const lift1 = f => Ma => [f(Ma[0])];

// lift2:: Monad M: (a -> b -> c) -> M a -> M b -> M c
const lift2 = f => Ma => Mb => [f(Ma[0])(Mb[0])];

// lift3 etc...

console.log(lift2(sum)(monad0)(monad1)); // [5]

Upvotes: 1

Reactgular
Reactgular

Reputation: 54771

Can someone please explain the purpose of each of these functions, and when each of them should be used?

lift() creates a new observable object but pipe() does not. pipe() follows the functional programming paradigm and lift() is object-oriented.

They both accept functions as input arguments, but the advantage of pipe() is that there is no extra observable created.

When you use lift() a single operator is attached to a new observable, and when this new observable is subscribed the attached operator intercepts the stream before it is subscribed too.

This is different from how pipe() works because an operator returns the same observable will yield no changes to the original observable.

pipe() was introduced after lift() and I think this is the preferred way to chain operators.

Upvotes: 8

JHeut
JHeut

Reputation: 305

I've found a nice in-depth discussion on this subject and the potential idea of removing Observable.lift in favor of Observable.pipe here: https://github.com/ReactiveX/rxjs/issues/2911

TL;DR

Now let's compare the "pure" lift and pipe signatures:

// I'm intentionally ignoring pipe's multiple operator function args,
// since we could redefine lift to also take multiple operator functions
type Operator = <T, R>(sink: Observer<R>) => Observer<T>
type lift = <T, R>(src: Observable<T>, op: Operator) => Observable<R>;
type pipe = <T, R>(op: Operator) => (src: Observable<T>) => Observable<R>
  • pipe's operator function maps an Observable to an Observable
  • lift's operator function maps an Observer to an Observer

This is just another way to represent the idea of either:

  • building an Observable chain down from the source to the sink
  • or building an Observer chain up from the sink to the source

Upvotes: 10

Related Questions