Search code examples
fp-ts

Refactoring after deprecation of getFoldableComposition, option, array et al


I spent some time last year trying to learn fp-ts. I've finally come around to using it in a project and a lot of my sample code has broken due to the recent refactoring. I've fixed a few of the breakages but am strugging with the others. It highlights a massive whole in my FP knowledge no doubt!

I had this:

import { strict as assert } from 'assert';
import { array } from 'fp-ts/Array';
import { getFoldableComposition, } from 'fp-ts/Foldable';
import { Monoid as MonoidString } from 'fp-ts/string'
import { none,some, option } from 'fp-ts/Option';

const F = getFoldableComposition(array, option)
assert.strictEqual(F.reduce([some('a'), none, some('c')], '', MonoidString.concat), 'ac')

getFoldableComposition, option and array are now deprecated. The comments on getFoldableComposition say to use reduce, foldMap or reduceRight instead, so, amongst other things, I tried this.

import { strict as assert } from 'assert';
import { reduceRight } from 'fp-ts/Foldable';
import { Monoid as MonoidString } from 'fp-ts/string'
import { some } from 'fp-ts/Option';

assert.strictEqual(reduceRight([some('a'), none, some('c')], '', MonoidString.concat), 'ac')

That's not even compiling, so obviously I'm way off base.

Could someone please show me the correct way to replace getFoldableComposition and, while we're at it, explain what is meant by 'Use small, specific instances instead' as well for option and array? Also, anything else I'm obviously doing wrong?

Thank you!


Solution

  • Let's start with your question

    what is meant by 'Use small, specific instances instead' as well for option and array?

    Prior to fp-ts v2.10.0, type class instances were grouped together as a single record implementing the interfaces of multiple classes, and the type class record was named after the data type for which the classes were defined. So for the Array module, array was exported containing all the instances; it had map for Functor and ap for Apply etc. For Option, the option record was exported with all the instances. And so on.

    Many functions, like getFoldableComposition and sequenceT are defined very generically using "higher-kinded types" and require you to pass in the type class instance for the data type you wanted the function to use. So, e.g., sequenceT requires you to pass an Apply instance like

    assert.deepEqual(
      sequenceT(O.option)([O.some(1), O.none]), 
      O.none
    )
    

    Requiring these big records of type classes instances to be passed around like that ended up making fp-ts not tree-shake well in application and library code, because JS bundlers couldn't statically tell which members of the type class record where being accessed and which weren't, so it ended up including all of them even if only one was used. That increases bundle size, which ultimately makes your app load slower for users and/or increases the bundle size of libraries consuming your library.

    The solution to this problem was to break the big type class records apart and give each type class its own record. So now each data type module exports small, individual type class instances and eventually the mega-instance record will be removed. So now you would use sequenceT like

    assert.deepEqual(
      sequenceT(O.Apply)([O.some(1), O.none]), 
      O.none
    )
    

    Now the bundler knows that only Apply methods are being used, and it can remove unused instances from the bundle.

    So the upshot of all this is to just not use the mega instance record anymore and only use the smaller instance records.

    Now for your code.

    The first thing I'll say is talk to the compiler. Your code should give you a compile error. What I'm seeing is this:

    reduceRight expected 2 arguments, but got 3.

    So you passed reduceRight too many arguments, so let's look at the signature:

    export declare function reduceRight<F extends URIS, G extends URIS>(
      F: Foldable1<F>,
      G: Foldable1<G>
    ): <B, A>(b: B, f: (a: A, b: B) => B) => (fga: Kind<F, Kind<G, A>>) => B
    

    First thing you should note, this function is curried and requires three invocations in order to fully evaluate (i.e. it is curried to three separate function calls). First it takes the type class instances, then the accumulator and reducing function, and finally it takes the data type we are reducing.

    So first it takes a Foldable instance for a type of kind Type -> Type, and another Foldable instance for another (or the same) type of kind Type -> Type. This is where the small vs big instance record comes into play. You'll pass SomeDataType.Foldable instead of SomeDataType.someDataType.

    Then it takes polymorphic type B of kind Type as the initial value for the reduce (aka the "accumulator") and a binary function which takes polymorphic type A of kind Type and B and returns B. This is the typical signature of a reduceRight.

    Then it takes a scary looking type which is making use of higher-kinded types. I would pronounce it as "F of G of A" or F<G<A>>. And finally it returns B, the reduced value.

    Sounds complicated, but hopefully after this it won't seem so bad.

    From looking at your code, it appears you want to reduce an Array<Option<string>> into a string. Array<Option<string>> is the higher-kinded type you want to specify. You just replace "F of G of A" with "Array of Option of string". So in the signature of reduceRight, F is the Foldable instance for Array and G is the Foldable instance for Option.

    If we pass those instances, we'll get back a reduceRight function specialized for an array of options.

    import * as A from 'fp-ts/Array'
    import * as O from 'fp-ts/Option'
    import { reduceRight } from 'fp-ts/Foldable'
    
    const reduceRightArrayOption: <B, A>(
      b: B, 
      f: (a: A, b: B) => B) => (fga: Array<O.Option<A>>) => B = 
      reduceRight(A.Foldable, O.Foldable)
    

    Then we call this reduce with the initial accumulator and a reducing function that takes the value inside Array<Option<?>> which is string and the type of the accumulator, which is also string. In your initial code, you were using concat for string. That will work here, and you'll find it on the Monoid<string> instance in the string module.

    import * as A from 'fp-ts/Array'
    import * as O from 'fp-ts/Option'
    import { reduceRight } from 'fp-ts/Foldable'
    import * as string from 'fp-ts/string'
    
    const reduceRightArrayOption: <B, A>(
      b: B, 
      f: (a: A, b: B) => B) => (fga: Array<O.Option<A>>) => B
      = reduceRight(A.Foldable, O.Foldable)
    
    const reduceRightArrayOptionStringToString: (fga: Array<O.Option<string>>) => string
      = reduceRightArrayOption("", string.Monoid.concat)
    

    Finally, it's ready to take our Array<O.Option<string>>.

    import * as assert from 'assert'
    import * as A from 'fp-ts/Array'
    import * as O from 'fp-ts/Option'
    import { reduceRight } from 'fp-ts/Foldable'
    import * as string from 'fp-ts/string'
    
    const reduceRightArrayOption: <B, A>(
      b: B, 
      f: (a: A, b: B) => B) => (fga: Array<O.Option<A>>) => B
      = reduceRight(A.Foldable, O.Foldable)
    
    const reduceRightArrayOptionStringToString: (fga: Array<O.Option<string>>) => string
      = reduceRightArrayOption("", string.Monoid.concat)
    
    const result = reduceRightArrayOptionStringToString([
      O.some('a'),
      O.none,
      O.some('c'),
    ])
    
    assert.strictEqual(result, "ac")
    

    To simplify all of this, we can use the more idiomatic pipe approach to calling reduceRight:

    import * as assert from "assert"
    import { reduceRight } from "fp-ts/Foldable"
    import * as string from "fp-ts/string"
    import * as O from "fp-ts/Option"
    import * as A from "fp-ts/Array"
    import { pipe } from "fp-ts/lib/function"
    
    assert.strictEqual(
      pipe(
        [O.some("a"), O.none, O.some("c")],
        reduceRight(A.Foldable, O.Foldable)(string.empty, string.Monoid.concat)
      ),
      "ac"
    )
    

    I know that was a lot, but hopefully it provides a little clarity about what's going on. reduceRight is very generic, in a way that almost no other TypeScript libraries attempt to be, so it's totally normal if it takes you a while to get your head around it. Higher-kinded types are not a built-in feature of TypeScript, and the way fp-ts does it is admittedly a bit of a hack to work around the limitations of TS. But keep playing around and experimenting. It'll all start to click eventually.