Skip to content

Instantly share code, notes, and snippets.

@OliverJAsh
Last active August 23, 2019 10:06
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save OliverJAsh/241a589bec9a5385c5fcd00448377b85 to your computer and use it in GitHub Desktop.
Save OliverJAsh/241a589bec9a5385c5fcd00448377b85 to your computer and use it in GitHub Desktop.
Naming pipeable operators

Naming pipeable operators

Many libraries are moving from instance methods to pipeable operators (e.g. RxJS, fp-ts, idb-keyval, Fluture, date-fns) because they have better bundling performance.

This works really well when you're only importing pipeable operators for one type (e.g. Observable from RxJS), but if we're working with multiple types (e.g. Observable and Option from fp-ts) in the same file, you will inevitably face the problem of naming conflicts. For example:

import { map } from "rxjs/operators";
import { map } from "fp-ts/lib/Option";

One solution is to use namespace imports:

import * as option from "fp-ts/es6/Option";
import { Option } from "fp-ts/es6/Option";
import { pipe } from "fp-ts/lib/pipeable";
import { Observable } from "rxjs";
import * as observable from "rxjs/operators";

declare const numberOption$: Observable<Option<number>>;

declare const add1: (n: number) => number;

numberOption$.pipe(
  observable.map(numberOption =>
    pipe(
      numberOption,
      option.map(add1)
    )
  )
);

But they add a lot of noise to code. Observe how much more concise our code is when using instance methods:

numberOption$.map(numberOption => numberOption.map(add1));

This might not seem like much, but it quickly gets out of hand if you need to use more operators.

@karol-majewski
Copy link

I wish it were possible to just import them like this:

import { Observable, map } from 'rxjs';
import { Option, pipe } from 'fp-ts';

declare const numberOption$: Observable<Option<number>>;

declare const add1: (n: number) => number;

numberOption$.pipe(
  map(value =>
    pipe(
      value,
      Option.map(add1)
    ),
  ),
);

Option would need to be a type and a value at the same time. This would require it to be a class. A class with static methods.

To make it tree-shakeable, it would need a do method as presented here.

Option
  .of(1)
  .do(
    map(value => value + 1),
    flatMap(value => Option.of(value - 1))
  )

@OliverJAsh
Copy link
Author

OliverJAsh commented Aug 22, 2019

@karol-majewski That do looks just like the pipe instance method that Observables have. I asked for it to be added to fp-ts: gcanti/fp-ts#937.

@OliverJAsh
Copy link
Author

OliverJAsh commented Aug 22, 2019

@bisubus
Copy link

bisubus commented Aug 23, 2019

Thanks for pinging me. Sadly, the use of instance methods can easily result in maintenance hell depending on the usage. Pieces of the app cannot be easily decoupled without painstaking management of operator imports. I ended up with having import 'rxjs/add/operator/map' in every module I used map and this was incredibly cumbersome. This is the reason why pipeable ops were introduced.

The example boils down to:

import { map as rxMap } from "rxjs/operators";
import { map as fpMap } from "fp-ts/lib/Option";

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment