Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tree shaking: function calls on module instantiation are not removed #1087

Closed
OliverJAsh opened this issue Jan 14, 2020 · 45 comments
Closed

Comments

@OliverJAsh
Copy link
Collaborator

🐛 Bug report

Current Behavior

Reduced test case

src/index.js:

import { max } from "fp-ts/es6/Ord";
console.log(max);

rollup.config.js:

import resolve from "@rollup/plugin-node-resolve";

export default {
  input: "src/index.js",
  output: {
    file: "target/rollup.js",
    format: "cjs"
  },
  plugins: [resolve()]
};

package.json:

{
  "dependencies": {
    "fp-ts": "^2.0.0",
    "@rollup/plugin-node-resolve": "^7.0.0",
    "rollup": "^1.29.0"
  }
}

Then run rollup -c rollup.config.js.

Expected behavior

The output of rollup should not include lots of unused code:

'use strict';

/**
 * The `Ord` type class represents types which support comparisons with a _total order_.
 *
 * Instances should satisfy the laws of total orderings:
 *
 * 1. Reflexivity: `S.compare(a, a) <= 0`
 * 2. Antisymmetry: if `S.compare(a, b) <= 0` and `S.compare(b, a) <= 0` then `a <-> b`
 * 3. Transitivity: if `S.compare(a, b) <= 0` and `S.compare(b, c) <= 0` then `S.compare(a, c) <= 0`
 *
 * See [Getting started with fp-ts: Ord](https://dev.to/gcanti/getting-started-with-fp-ts-ord-5f1e)
 *
 * @since 2.0.0
 */
/**
 * Take the maximum of two values. If they are considered equal, the first argument is chosen
 *
 * @since 2.0.0
 */
function max(O) {
    return function (x, y) { return (O.compare(x, y) === -1 ? y : x); };
}

console.log(max);

Actual behaviour

The output of rollup includes lots of unused code:

Show code snippet
'use strict';

/**
 * @since 2.0.0
 */
/**
 * @since 2.0.0
 */
function identity(a) {
    return a;
}

var isFunctor = function (I) { return typeof I.map === 'function'; };
var isContravariant = function (I) { return typeof I.contramap === 'function'; };
var isFunctorWithIndex = function (I) { return typeof I.mapWithIndex === 'function'; };
var isApply = function (I) { return typeof I.ap === 'function'; };
var isChain = function (I) { return typeof I.chain === 'function'; };
var isBifunctor = function (I) { return typeof I.bimap === 'function'; };
var isExtend = function (I) { return typeof I.extend === 'function'; };
var isFoldable = function (I) { return typeof I.reduce === 'function'; };
var isFoldableWithIndex = function (I) { return typeof I.reduceWithIndex === 'function'; };
var isAlt = function (I) { return typeof I.alt === 'function'; };
var isCompactable = function (I) { return typeof I.compact === 'function'; };
var isFilterable = function (I) { return typeof I.filter === 'function'; };
var isFilterableWithIndex = function (I) {
    return typeof I.filterWithIndex === 'function';
};
var isProfunctor = function (I) { return typeof I.promap === 'function'; };
var isSemigroupoid = function (I) { return typeof I.compose === 'function'; };
var isMonadThrow = function (I) { return typeof I.throwError === 'function'; };
function pipeable(I) {
    var r = {};
    if (isFunctor(I)) {
        var map = function (f) { return function (fa) { return I.map(fa, f); }; };
        r.map = map;
    }
    if (isContravariant(I)) {
        var contramap = function (f) { return function (fa) { return I.contramap(fa, f); }; };
        r.contramap = contramap;
    }
    if (isFunctorWithIndex(I)) {
        var mapWithIndex = function (f) { return function (fa) { return I.mapWithIndex(fa, f); }; };
        r.mapWithIndex = mapWithIndex;
    }
    if (isApply(I)) {
        var ap = function (fa) { return function (fab) { return I.ap(fab, fa); }; };
        var apFirst = function (fb) { return function (fa) {
            return I.ap(I.map(fa, function (a) { return function () { return a; }; }), fb);
        }; };
        r.ap = ap;
        r.apFirst = apFirst;
        r.apSecond = function (fb) { return function (fa) {
            return I.ap(I.map(fa, function () { return function (b) { return b; }; }), fb);
        }; };
    }
    if (isChain(I)) {
        var chain = function (f) { return function (ma) { return I.chain(ma, f); }; };
        var chainFirst = function (f) { return function (ma) { return I.chain(ma, function (a) { return I.map(f(a), function () { return a; }); }); }; };
        var flatten = function (mma) { return I.chain(mma, identity); };
        r.chain = chain;
        r.chainFirst = chainFirst;
        r.flatten = flatten;
    }
    if (isBifunctor(I)) {
        var bimap = function (f, g) { return function (fa) { return I.bimap(fa, f, g); }; };
        var mapLeft = function (f) { return function (fa) { return I.mapLeft(fa, f); }; };
        r.bimap = bimap;
        r.mapLeft = mapLeft;
    }
    if (isExtend(I)) {
        var extend = function (f) { return function (wa) { return I.extend(wa, f); }; };
        var duplicate = function (wa) { return I.extend(wa, identity); };
        r.extend = extend;
        r.duplicate = duplicate;
    }
    if (isFoldable(I)) {
        var reduce = function (b, f) { return function (fa) { return I.reduce(fa, b, f); }; };
        var foldMap = function (M) {
            var foldMapM = I.foldMap(M);
            return function (f) { return function (fa) { return foldMapM(fa, f); }; };
        };
        var reduceRight = function (b, f) { return function (fa) { return I.reduceRight(fa, b, f); }; };
        r.reduce = reduce;
        r.foldMap = foldMap;
        r.reduceRight = reduceRight;
    }
    if (isFoldableWithIndex(I)) {
        var reduceWithIndex = function (b, f) { return function (fa) {
            return I.reduceWithIndex(fa, b, f);
        }; };
        var foldMapWithIndex = function (M) {
            var foldMapM = I.foldMapWithIndex(M);
            return function (f) { return function (fa) { return foldMapM(fa, f); }; };
        };
        var reduceRightWithIndex = function (b, f) { return function (fa) {
            return I.reduceRightWithIndex(fa, b, f);
        }; };
        r.reduceWithIndex = reduceWithIndex;
        r.foldMapWithIndex = foldMapWithIndex;
        r.reduceRightWithIndex = reduceRightWithIndex;
    }
    if (isAlt(I)) {
        var alt = function (that) { return function (fa) { return I.alt(fa, that); }; };
        r.alt = alt;
    }
    if (isCompactable(I)) {
        r.compact = I.compact;
        r.separate = I.separate;
    }
    if (isFilterable(I)) {
        var filter = function (predicate) { return function (fa) {
            return I.filter(fa, predicate);
        }; };
        var filterMap = function (f) { return function (fa) { return I.filterMap(fa, f); }; };
        var partition = function (predicate) { return function (fa) {
            return I.partition(fa, predicate);
        }; };
        var partitionMap = function (f) { return function (fa) { return I.partitionMap(fa, f); }; };
        r.filter = filter;
        r.filterMap = filterMap;
        r.partition = partition;
        r.partitionMap = partitionMap;
    }
    if (isFilterableWithIndex(I)) {
        var filterWithIndex = function (predicateWithIndex) { return function (fa) { return I.filterWithIndex(fa, predicateWithIndex); }; };
        var filterMapWithIndex = function (f) { return function (fa) {
            return I.filterMapWithIndex(fa, f);
        }; };
        var partitionWithIndex = function (predicateWithIndex) { return function (fa) { return I.partitionWithIndex(fa, predicateWithIndex); }; };
        var partitionMapWithIndex = function (f) { return function (fa) {
            return I.partitionMapWithIndex(fa, f);
        }; };
        r.filterWithIndex = filterWithIndex;
        r.filterMapWithIndex = filterMapWithIndex;
        r.partitionWithIndex = partitionWithIndex;
        r.partitionMapWithIndex = partitionMapWithIndex;
    }
    if (isProfunctor(I)) {
        var promap = function (f, g) { return function (fa) { return I.promap(fa, f, g); }; };
        r.promap = promap;
    }
    if (isSemigroupoid(I)) {
        var compose = function (that) { return function (fa) { return I.compose(fa, that); }; };
        r.compose = compose;
    }
    if (isMonadThrow(I)) {
        var fromOption = function (onNone) { return function (ma) {
            return ma._tag === 'None' ? I.throwError(onNone()) : I.of(ma.value);
        }; };
        var fromEither = function (ma) {
            return ma._tag === 'Left' ? I.throwError(ma.left) : I.of(ma.right);
        };
        var fromPredicate = function (predicate, onFalse) { return function (a) { return (predicate(a) ? I.of(a) : I.throwError(onFalse(a))); }; };
        var filterOrElse = function (predicate, onFalse) { return function (ma) { return I.chain(ma, function (a) { return (predicate(a) ? I.of(a) : I.throwError(onFalse(a))); }); }; };
        r.fromOption = fromOption;
        r.fromEither = fromEither;
        r.fromPredicate = fromPredicate;
        r.filterOrElse = filterOrElse;
    }
    return r;
}

/**
 * The `Ord` type class represents types which support comparisons with a _total order_.
 *
 * Instances should satisfy the laws of total orderings:
 *
 * 1. Reflexivity: `S.compare(a, a) <= 0`
 * 2. Antisymmetry: if `S.compare(a, b) <= 0` and `S.compare(b, a) <= 0` then `a <-> b`
 * 3. Transitivity: if `S.compare(a, b) <= 0` and `S.compare(b, c) <= 0` then `S.compare(a, c) <= 0`
 *
 * See [Getting started with fp-ts: Ord](https://dev.to/gcanti/getting-started-with-fp-ts-ord-5f1e)
 *
 * @since 2.0.0
 */
/**
 * @since 2.0.0
 */
var URI = 'Ord';
// default compare for primitive types
var compare = function (x, y) {
    return x < y ? -1 : x > y ? 1 : 0;
};
function strictEqual(a, b) {
    return a === b;
}
/**
 * @since 2.0.0
 */
var ordNumber = {
    equals: strictEqual,
    compare: compare
};
/**
 * Take the maximum of two values. If they are considered equal, the first argument is chosen
 *
 * @since 2.0.0
 */
function max(O) {
    return function (x, y) { return (O.compare(x, y) === -1 ? y : x); };
}
/**
 * @since 2.0.0
 */
function fromCompare(compare) {
    var optimizedCompare = function (x, y) { return (x === y ? 0 : compare(x, y)); };
    return {
        equals: function (x, y) { return optimizedCompare(x, y) === 0; },
        compare: optimizedCompare
    };
}
/**
 * @since 2.0.0
 */
var ord = {
    URI: URI,
    contramap: function (fa, f) { return fromCompare(function (x, y) { return fa.compare(f(x), f(y)); }); }
};
var contramap = pipeable(ord).contramap;
/**
 * @since 2.0.0
 */
var ordDate = ord.contramap(ordNumber, function (date) { return date.valueOf(); });

console.log(max);

Reproducible example

Suggested solution(s)

Add annotations/comments to indicate no side effects, e.g. in fp-ts/es6/Ord.js:

-var contramap = pipeable(ord).contramap;
+var pipeableResult = /*#__PURE__*/pipeable(ord);
+var contramap = pipeableResult.contramap;
 export { 
 /**
  * @since 2.0.0
  */
 contramap };
 /**
  * @since 2.0.0
  */
-export var ordDate = ord.contramap(ordNumber, function (date) { return date.valueOf(); });
+export var ordDate = /*#__PURE__*/ord.contramap(ordNumber, function (date) { return date.valueOf(); });

This fixes the issue when using Rollup. Reduced test case.

Note this does not fix the issue with webpack, for some reason: webpack/webpack#10253

Additional context

Your environment

Which versions of fp-ts are affected by this issue? Did this work in previous versions of fp-ts?

Software Version(s)
fp-ts See above
TypeScript See above
@Andarist
Copy link

Keep in mind that this:

-var contramap = pipeable(ord).contramap;
+var pipeableResult = /*#__PURE__*/pipeable(ord);
+var contramap = pipeableResult.contramap;

won't really help much, because #__PURE__ only works on call & new expressions and you access a property later which will cause a bailout on this case (because a getter is not known to be a side-effect free). I've proposed extending #__PURE__ with getters support, but it has been declined by Terser maintainers.

@OliverJAsh
Copy link
Collaborator Author

Note this does not fix the issue with webpack, for some reason: webpack/webpack#10253

I discovered that this is because webpack is not as aggressive as Rollup: webpack/webpack#10253 (comment)

In turn, this is because Terser does not respect "pure" comments on getters: terser/terser#513. (As pointed out by @Andarist above.)

Therefore we would need to do this:

diff --git a/node_modules/fp-ts/es6/Ord.js b/node_modules/fp-ts/es6/Ord.js
index c6d925d..438bf26 100644
--- a/node_modules/fp-ts/es6/Ord.js
+++ b/node_modules/fp-ts/es6/Ord.js
@@ -137,8 +137,8 @@ export function getSemigroup() {
 }
 const M = {
     // tslint:disable-next-line: deprecation
-    concat: getSemigroup().concat,
-    empty: fromCompare(() => 0)
+    concat: /*#__PURE__*/(() => getSemigroup().concat)(),
+    empty: /*#__PURE__*/fromCompare(() => 0)
 };
 /**
  * Returns a `Monoid` such that:
@@ -245,7 +245,8 @@ export const ord = {
     URI,
     contramap: (fa, f) => fromCompare((x, y) => fa.compare(f(x), f(y)))
 };
-const { contramap } = pipeable(ord);
+const pipeableResult = /*#__PURE__*/pipeable(ord);
+const contramap = /*#__PURE__*/(() => pipeableResult.contramap)();
 export { 
 /**
  * @since 2.0.0
@@ -254,4 +255,4 @@ contramap };
 /**
  * @since 2.0.0
  */
-export const ordDate = ord.contramap(ordNumber, date => date.valueOf());
+export const ordDate = /*#__PURE__*/ord.contramap(ordNumber, date => date.valueOf());

Here is a reduced test case where you can see how tree shaking works in both Rollup and webpack: https://github.com/OliverJAsh/tree-shaking-test/tree/fp-ts-issue-1087.

If you run patch-package, it will apply the above patch to the fp-ts Node module. After that you will see tree shaking works significantly better.

@gcanti What do you think about making these changes?

  • Where a function is called on module instantiation, add a "pure" annotation comment
  • Where an object property/getter is accessed on module instantiation, wrap it in an IIFE and add a "pure" annotation comment

@OliverJAsh
Copy link
Collaborator Author

@gcanti
Copy link
Owner

gcanti commented Jan 15, 2020

What do you think about making these changes?

@OliverJAsh

  • in a realistic usage of fp-ts, is there a tangible benefit? The pipeable module is used in almost all modules
  • can those changes be automated?

@OliverJAsh
Copy link
Collaborator Author

  • in a realistic usage of fp-ts, is there a tangible benefit? The pipeable module is used in almost all modules

Good question. I don't have an immediate answer, but I'll give it some thought. The reason I ended up going down this rabbit hole is because I noticed that we were bundling more code than we actually use, e.g. our bundle contains Ord and Show even though we never use these directly nor indirectly (e.g. via methods which depend on them).

@Andarist
Copy link

in a realistic usage of fp-ts, is there a tangible benefit? The pipeable module is used in almost all modules

It's not only about pipe being called but also about arguments passed to it. If everything is wrapped in pipe and pipe is not marked as pure then it, in turn, holds to everything and nothing can be tree-shaked.

The very similar situation was in Ramda - everything is curried, but by making similar changes some time ago I was able to make tree-shaking work for it 100%. If you import a single function from Ramda (since 0.25) then you will end up only with that single function and with what it depends on.

@OliverJAsh
Copy link
Collaborator Author

OliverJAsh commented Jan 16, 2020

If you import a single function from Ramda (since 0.25) then you will end up only with that single function and with what it depends on.

If fp-ts wanted to achieve this, I think we'd have to remove pipeable because it's dynamic, and as soon as it's used for one import, it will bring everything else with it—all exports must be defined statically?

@Andarist
Copy link

Oh, I've looked into what pipeable does under the hood - so yes, it would have to be removed to improve the tree-shaking story. It technically maybe could become a babel macro or something and just compile away, but that would rather only complicate things for it.

@pe3
Copy link

pe3 commented Feb 24, 2020

@Andarist I remember there was a period of hacks and Rollup-plugins for making Ramda including builds smaller:
https://github.com/polytypic/ramda-rollup-hack
https://github.com/idmitriev/rollup-plugin-ramda

@Andarist
Copy link

There was, but such hacks are no longer needed for ramda. IMHO would be better to fix this idiomatically here rather than introduce custom hacks like this.

@mikearnaldi
Copy link
Contributor

mikearnaldi commented May 15, 2020

I have been working on making tree shaking work for the modules I use in matechs-effect, I can confirm that pipeable is a very problematic component.

Basically as noticed before having pipeable make the whole module non shakable, furthermore the sideEffect flag is only on a module level so whenever we include any function from a module we are including the full module.

I have changed the module structure to isolate each function in a single file basically one module per function and shaking is happly working.

Exmple at:
https://github.com/Matechs-Garage/matechs-effect/tree/features/tree-shaking/shaking/core/src/Array

One other thing that is important is how packages are published currently we have a dual /lib /es6 and this is not optimal, having a material-ui like publish mechanism where in each sub-module there is a package.json listing the es6 variant in it is better because the same code (even if required by a library) gets propertly indexed for the shaking algorithms, basically imports will have to look like "fp-ts/Array" where Array has itself a package.json so even if "fp-ts/Array" is required by a dependency we don't end up with the es5 version.

Slighlty unrelated (more an opinion) large typeclasses are also an enemy of shakability, for example option has a wide number of instances in it while for shakability is preferred to have variants with only for example Applicative or Monad like:
https://github.com/Matechs-Garage/matechs-effect/blob/features/tree-shaking/shaking/core/src/Option/instances.ts
&
https://github.com/Matechs-Garage/matechs-effect/blob/features/tree-shaking/shaking/core/src/Option/monad.ts

I am doing this for the modules listed at:
https://github.com/Matechs-Garage/matechs-effect/tree/features/tree-shaking/shaking/core/src

In case we decide to persue this for the core fp-ts I would be able to backport those here, the change will unfortunately be breaking because of the module structure)

--
update: rollup is slightly better compared to webpack, it will shake full modules (without pipeable)

@gcanti
Copy link
Owner

gcanti commented May 16, 2020

What do you think about making these changes?

  • Where a function is called on module instantiation, add a "pure" annotation comment
  • Where an object property/getter is accessed on module instantiation, wrap it in an IIFE and add a "pure" annotation comment

@OliverJAsh I would give it a try, starting from handling pipeable calls first, which should give the greatest benefit.

So, for example, in Option.ts, this...

const {
  alt,
  ap,
  apFirst,
  apSecond,
  chain,
  chainFirst,
  duplicate,
  extend,
  filter,
  filterMap,
  flatten,
  foldMap,
  map,
  partition,
  partitionMap,
  reduce,
  reduceRight,
  compact,
  separate,
  fromEither
} = pipeable(option)

...will be replaced by

const pipeables = /*#__PURE__*/ pipeable(option)
const alt = /*#__PURE__*/ (() => pipeables.alt)()
const ap = /*#__PURE__*/ (() => pipeables.ap)()
const apFirst = /*#__PURE__*/ (() => pipeables.apFirst)()
const apSecond = /*#__PURE__*/ (() => pipeables.apSecond)()
const chain = /*#__PURE__*/ (() => pipeables.chain)()
const chainFirst = /*#__PURE__*/ (() => pipeables.chainFirst)()
const duplicate = /*#__PURE__*/ (() => pipeables.duplicate)()
const extend = /*#__PURE__*/ (() => pipeables.extend)()
const filter = /*#__PURE__*/ (() => pipeables.filter)()
const filterMap = /*#__PURE__*/ (() => pipeables.filterMap)()
const flatten = /*#__PURE__*/ (() => pipeables.flatten)()
const foldMap = /*#__PURE__*/ (() => pipeables.foldMap)()
const map = /*#__PURE__*/ (() => pipeables.map)()
const partition = /*#__PURE__*/ (() => pipeables.partition)()
const partitionMap = /*#__PURE__*/ (() => pipeables.partitionMap)()
const reduce = /*#__PURE__*/ (() => pipeables.reduce)()
const reduceRight = /*#__PURE__*/ (() => pipeables.reduceRight)()
const compact = /*#__PURE__*/ (() => pipeables.compact)()
const separate = /*#__PURE__*/ (() => pipeables.separate)()
const fromEither = /*#__PURE__*/ (() => pipeables.fromEither)()

right?

I noticed that we were bundling more code than we actually use

If I put up a branch with such a changes, would you be able to check whether they are actually working?

@Andarist
Copy link

I can volunteer to checking out the changes

@OliverJAsh
Copy link
Collaborator Author

@gcanti

right?

👌

If I put up a branch with such a changes, would you be able to check whether they are actually working?

Absolutely!

@mikearnaldi
Copy link
Contributor

Another alternative would be to kill pipeable given it will still add a lot of additional stuff to the bundle, we should also expose the data-last functions to allow other modules to not depend on the full instance, a prototype PR with Either is:

#1212

@giogonzo
Copy link
Collaborator

giogonzo commented May 17, 2020

If I understand it correctly, pipeable could be left there for anyone not concerned with tree shaking to quickly implement pipeable instances, but not used internally

@gcanti
Copy link
Owner

gcanti commented May 17, 2020

@Andarist @OliverJAsh I put up a 1087 branch (you can install it by running npm i gcanti/fp-ts#1087, the es6 folder is commited in).

Here's the repo I'm using to test different snippets https://github.com/gcanti/tree-shaking-test

Some examples:

Array snippet

rollup:

webpack

TaskEither snippet

rollup:

webpack


Changes so far:

  • added /*@__PURE__*/ to pipeable calls
  • added /*@__PURE__*/ to monad transformers calls

@OliverJAsh
Copy link
Collaborator Author

OliverJAsh commented May 18, 2020

@gcanti Could you add the lib folder to that branch? My webpack config relies on it, because Node doesn't support ES Modules yet… so without the lib folder I can't build my app to test this!

@gcanti
Copy link
Owner

gcanti commented May 18, 2020

@OliverJAsh done

@OliverJAsh
Copy link
Collaborator Author

That definitely fixes the issue I described in my original post.

When I tried it on the Unsplash app, the total size of fp-ts in our bundle decreased from 19 KB to 17 KB gzipped.

@Andarist
Copy link

I can also confirm this improves things a little bit - the only "big" thing that could be done to improve the situation more is removing pipeable altogether, but this has severe API impact so not sure if you are willing to go down this road.

@raveclassic
Copy link
Collaborator

I would go for that, I even suggested to move to curried data-last typeclasses in 3.0.

@mikearnaldi
Copy link
Contributor

I would go for that, I even suggested to move to curried data-last typeclasses in 3.0.

That should be carefully evaluate I am the opinion that it will have severe performance implications.

I can confirm removing pipeable improves significantly shakability (like also removing usage of transformers).

Another step would be to skin the typeclasses, instead of having 1 that bundles all toghether we might have many smaller to be used ad hoc in combinators, for examle instead of one implementing Applicative & Foldable have 2 implementing each separatedly.

@raveclassic
Copy link
Collaborator

That should be carefully evaluate I am the opinion that it will have severe performance implications.

I'm not sure about that. Most of the time we use curried data-last top-level functions which are produced by pipeable which makes extra call to original uncurried version. So this place should be improved.
The functions that take typeclass instance either call pipeable themselves to get generic curried functions (this should also be improved) or use the instance directly which ends in code inconsistency. And in such case I suppose we could sacrifice performance in favor of solving the whole bunch of problems by dropping pipeable. Still I think V8 should handle extra call in curried version pretty well.

@mikearnaldi
Copy link
Contributor

@raveclassic I did a test a while ago making the base of matechs-effect curried and I was in fact paying a good 2x - probably it can still be worked around by not using the instance where perf count but we should do proper testing before. In respect of pipeable if you remove it you will not have the calls anyway regardless of the typeclass structure - anyway discussion for a different thread

@gcanti
Copy link
Owner

gcanti commented May 19, 2020

discussion for a different thread

#1216 if anyone wants to chime in

the only "big" thing that could be done to improve the situation more is removing pipeable altogether, but this has severe API impact so not sure if you are willing to go down this road.

AFAIK this can be done without breaking changes, it's just a lot of manual work

@gcanti
Copy link
Owner

gcanti commented May 26, 2020

the only "big" thing that could be done to improve the situation more is removing pipeable altogether, but this has severe API impact so not sure if you are willing to go down this road.

AFAIK this can be done without breaking changes, it's just a lot of manual work

@Andarist @OliverJAsh I removed import { pipeable } from './pipeable' from all modules and updated the 1087 branch, could you please try it out?

Here's the results in my test repo (https://github.com/gcanti/tree-shaking-test)

Array snippet

rollup:

webpack:

TaskEither snippet

rollup:

webpack:

Task snippet

rollup:

webpack:

@OliverJAsh
Copy link
Collaborator Author

@gcanti Looks like it's missing the lib folder again?

@gcanti
Copy link
Owner

gcanti commented May 26, 2020

@OliverJAsh opsss... sorry (commented out the wrong line in .gitignore) should be ok now

@OliverJAsh
Copy link
Collaborator Author

Thanks! Unfortunately I can't test this because it seems to have broken integration with https://github.com/devexperts/remote-data-ts

image

Is there a quick patch I can make to that library to fix this? Then I will be able to run some tests.

@OliverJAsh
Copy link
Collaborator Author

@gcanti
Copy link
Owner

gcanti commented May 26, 2020

@OliverJAsh weird, honestly I can't see how my refactoring could affect that library. Is there a way I can repro?

I added a RemoteData example in my test repo and it's compiling fine

@OliverJAsh
Copy link
Collaborator Author

OliverJAsh commented May 26, 2020

It's working now. 😕 False alarm.

In our codebase:

It looks like our usage of fp-ts has changed since I last reported test results, and I can't remember which commit I tested it against last time. I'd be curious to test the changes without the removal of pipeable again, so I can see what difference that makes on its own (to make sure it is actually working for us). If you're able to publish those changes on a separate branch then I can compare all 3: 2.6.1, after #1087 (comment), and then after #1087 (comment) (the 1087 branch).

@gcanti
Copy link
Owner

gcanti commented May 27, 2020

If you're able to publish those changes on a separate branch then I can compare all 3

@OliverJAsh branch 1087-PURE-only

@OliverJAsh
Copy link
Collaborator Author

OliverJAsh commented May 27, 2020

Thanks. I'm seeing exactly the same size for both 1087-PURE-only and 1087. Not sure whether that means the additional changes in 1087 are not working, or whether it's just because we're unable to benefit from these changes because all exports are actually used.

@gcanti
Copy link
Owner

gcanti commented May 27, 2020

it's just because we're unable to benefit from these changes because all exports are actually used

@OliverJAsh ^ this is the case I guess. In a fp-ts heavy based application the benefit should be less apparent, however in my test repo, which contains small scripts, the benefit is tangible.

Here's the stats

Array snippet

rollup:

webpack:

TaskEither snippet

rollup:

webpack:

Task snippet

rollup:

webpack:

Ok thanks for the help, I'm going to release v2.6.2

@OliverJAsh
Copy link
Collaborator Author

Awesome! Thanks for your work on this.

@gcanti
Copy link
Owner

gcanti commented May 27, 2020

Closed by #1220

@gcanti gcanti closed this as completed May 27, 2020
@gcanti
Copy link
Owner

gcanti commented Jun 7, 2020

@OliverJAsh I'm working on v3 and there are some ideas that I would like to backport to v2 and see if they make any difference with respect to tree shaking

  • (v2.6.3) export sequence (which is pipeable but currently is weirdly not exported)
  • (v2.6.3) export a pipeable version of traverse
  • (v2.6.3) remove the monad transformers imports
  • (v2.6.3) move pipe to function.ts
  • (v2.7.0) split the mega instances (for example either into functorEither, applyEither, etc...)

Note: top level traverse / sequence functions will allow to not import A.array which is huge

@gcanti gcanti reopened this Jun 7, 2020
@raveclassic
Copy link
Collaborator

@gcanti

I'm working on v3

Yaaay! If you need any help, please let me know!

@gcanti
Copy link
Owner

gcanti commented Jun 7, 2020

@raveclassic thanks, you can get a sneak pick on branch 3.0.0 (and 3.0.0-es6 which is installable from GitHub) but please keep in mind that I'm basically just experimenting, sometimes with big breaking refactorings, so I can't ensure any stability whatsoever in this early phase.

@gcanti
Copy link
Owner

gcanti commented Jun 8, 2020

Closed by #1232

@OliverJAsh just released v2.6.3 could you please try it out?

My TaskEither snippet

import * as _ from "fp-ts/es6/TaskEither";
import { pipe } from "fp-ts/es6/pipeable";

pipe(
  _.right(1),
  _.map(n => n + 1),
  _.chain(n => _.right(n + 1)),
  _.swap
);

went from 11K to 7K (rollup)

Stats:

Most benefits should come from replacing all occurrences of A.array.sequence (or A.array.traverse) with A.sequence (or A.traverse)

@OliverJAsh
Copy link
Collaborator Author

  • 2.6.2: 22.96 KB (gzipped)
  • 2.6.3 with all sequence/traverse references updated to use the new pipeable versions (🎉): 22.25 KB

@gcanti
Copy link
Owner

gcanti commented Jun 9, 2020

@OliverJAsh nice, thank you!

@gcanti gcanti closed this as completed Jun 9, 2020
@gcanti
Copy link
Owner

gcanti commented Jun 10, 2020

I know wither / wilt are less used in the wild than sequence / traverse but here's a PR for them #1235

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants