Flow: Type inference and functionnal programming

Created on 19 Nov 2018  Â·  16Comments  Â·  Source: facebook/flow

When using functionnal programming it is sometimes difficult to work with flow because the type is not always infered correcly.

Take this simple example:

type Tests = Array<number | string>;

function testFn(tests: Tests) {
  tests
    .filter(test => typeof test !== 'string')
    .map(test => Math.max(test, 10)); // Cannot call `Math.max` because string [1] is incompatible with number [2] in array element.
}

Link: https://flow.org/try/#0FAegVABAAgZgNgewO4TCYwAuBPADgUwgBV8BnTUiAXggEEAnegQ2wB4A7AVwFsAjfehAA+EcvQCW7AOYA+ANwYYndgGNM4hOwiYymAGLsAFDvKkAXMV2kAlBADewCNquOnEAHQxxcHfWO7qGW08fAQYZ3IIAEIqGgByMUkpOOtXJ3duJlx-SKoggFkmTAALDKYADxzMABoIAEYABmtrBQBfYCA

I know this case may be hard to solve and typescript, in this scenario, is not better: http://www.typescriptlang.org/play/#src=type%20Tests%20%3D%20Array%3Cnumber%20%7C%20string%3E%3B%0D%0A%0D%0Afunction%20testFn(tests%3A%20Tests)%20%7B%0D%0A%20%20tests%0D%0A%20%20%20%20.filter(test%20%3D%3E%20typeof%20test%20!%3D%3D%20'string')%0D%0A%20%20%20%20.map(test%20%3D%3E%20Math.max(test%2C%2010))%3B%0D%0A%7D

But with typescript I can override the type of the argument, to enforce it type to be number and this does solve my type problem: http://www.typescriptlang.org/play/#src=type%20Tests%20%3D%20Array%3Cnumber%20%7C%20string%3E%3B%0D%0A%0D%0Afunction%20testFn(tests%3A%20Tests)%20%7B%0D%0A%20%20tests%0D%0A%20%20%20%20.filter(test%20%3D%3E%20typeof%20test%20!%3D%3D%20'string')%0D%0A%20%20%20%20.map((test%3A%20number)%20%3D%3E%20Math.max(test%2C%2010))%3B%0D%0A%7D

But with flow, I still get an error: https://flow.org/try/#0FAegVABAAgZgNgewO4TCYwAuBPADgUwgBV8BnTUiAXggEEAnegQ2wB4A7AVwFsAjfehAA+EcvQCW7AOYA+ANwYYndgGNM4hOwiYymAGLsAFDvKkAXMV2kAlBADewCNquOnEAHQxxcHfWO7qGW08fAQYZ3IIAEIqGgByMUkpOOtXJ3duJlxDf3ILLj4BWyoggFkmTAALDKYAD1zMABoIAEYABmtrBQBfYCA

How should I do it with flow ?

refinements

Most helpful comment

Actually, that TS example reminded me that in Flow you can give type hints too — when you have a function with an explicit generic parameter. So if a function has, for example a generic parameter function doStuff<T>(...){...} you can call it with const result = doStuff<string>(....);.

The problem with filter is that the definition in the Flow lib does not use a generic parameter for filter itself, only inside for the callback:

https://github.com/facebook/flow/blob/v0.91/lib/core.js#L243 (linking to Array, there are additional definitions for the various array classes, from TypedArray to $ReadOnlyArray).

So I copied the whole class definition into a <PROJECT_HOME>/flow-typed/flow-lib-fixes.js file that I already have anyway because some built-in definitions are incomplete or broken. Then I changed the definition of (Array's) filter ever so slightly.

Currently built-in:

filter(callbackfn: (value: T, index: number, array: Array<T>) => any, thisArg?: any): Array<T>;

My version, only adding <T> just after the function name and leaving the rest unchanged:

filter<T>(callbackfn: (value: T, index: number, array: Array<T>) => any, thisArg?: any): Array<T>;

I then rewrote OPs example, merely adding a type hint to the filter call:

type Tests = Array<number | string>;

function testFn(tests: Tests) {
    tests
    .filter<number>(test => typeof test !== 'string')
    .map(test => Math.max(test, 10));
}

and it works. If I change the type hint to "string" Flow complains again. So the type hint is accepted.


I don't know what having/adding a generic parameter to the function name actually does inside Flow, vs. only using it deeper inside a function definition (what is clear, also from trying to give a hint when you don't have the generic type on the function name is that functions without it cannot be called with such a hint, you would get error Cannot call non-polymorphic function type with type arguments).

Maybe it would be helpful to add it to all those functions so that they can take such hints? A feature that also isn't documented anywhere I think. Maybe someone like @wchargin or @jbrown215 would be able to say something about what I did here?

All 16 comments

.filter(Boolean) will infer type

type Tests = Array<number | string>;

function testFn(tests: Tests) {
  tests
    .map(test => typeof test === 'string' ? null : test)
    .filter(Boolean)
    .map(test => Math.max(test, 10));
}

https://flow.org/try/#0FAegVABAAgZgNgewO4TCYwAuBPADgUwgBV8BnTUiAXggEEAnegQ2wB4A7AVwFsAjfehAA+EcvQCW7AOYA+ANwYYndgGNM4hOwiYymAGLsAFDvKkAXMV2kAlBADewCNquOIASAB03JrmO7qMtp4+AgwzuTUVDQA5GKSUtEQAPwQXHBwEBYmmNauTh4w4nA69IYAQggIcPhM7LlO+d6+2QEQALJMmAAWXkwAHn7kADQQAIwADNbWCgC+wEA

Thanks @zerobias for the answer.

The problème here is that you have transformed the logic.
If you have the value 0 in the array, it will be skipped in the last map.

And this is a simple example. It might be way more complicated...
Is here a way to solve the problem without changing the logic ? (using types)

You can use reduce instead of filter.

type Tests = Array<number | string>;

function testFn(tests: Tests) {
  tests
    .reduce((accumulator, current) => {
        if (typeof current !== 'string') 
          accumulator.push(current);
        return accumulator;
    }, [])
    .map(test => Math.max(test, 10));
}

https://flow.org/try/#0FAegVABAAgZgNgewO4TCYwAuBPADgUwgBV8BnTUiAXggEEAnegQ2wB4A7AVwFsAjfehAA+EcvQCW7AOYA+ANwYYndgGNM4hOwiYymAGLsAFDvKkAXMV2kAlBADewCNquOnEAHT18AE04r8hoZMKio8nHBMmAj0ADQQoYz47Ji2VDL2rk4AkOIwEMZ4+Ah5CV7JEACEVDQA5GKSUjW2mW6twaHc4ZHR7ricpAAWhqVJKQqtWV6YnPRa7WERUfTjEFkAvnEA2gC61i3u3Ey4xrrU6QCykQMHTAAeJ+RxAIwADNbWCmvAQA

filter does not change the type of array element. .filter(Boolean) is only exception.

Thanks @alexandersorokin for yout answer.

It is just too bad that you have to use reduce for filtering your array instead of just using filter. It does not make sense... :cry:

@tonai the main issue that you not just filter things but implicitly change a type of them that is much more significant change than selecting stuff from the collection

Actually with typescript, if you add a type annotation to the predicate, it works:

type Tests = Array<number | string>;

function testFn(tests: Tests) {
  tests
    .filter((test): test is number => typeof test !== 'string') // resulting array is number[]
    .map(test => Math.max(test, 10)); // test is number
}

Actually, that TS example reminded me that in Flow you can give type hints too — when you have a function with an explicit generic parameter. So if a function has, for example a generic parameter function doStuff<T>(...){...} you can call it with const result = doStuff<string>(....);.

The problem with filter is that the definition in the Flow lib does not use a generic parameter for filter itself, only inside for the callback:

https://github.com/facebook/flow/blob/v0.91/lib/core.js#L243 (linking to Array, there are additional definitions for the various array classes, from TypedArray to $ReadOnlyArray).

So I copied the whole class definition into a <PROJECT_HOME>/flow-typed/flow-lib-fixes.js file that I already have anyway because some built-in definitions are incomplete or broken. Then I changed the definition of (Array's) filter ever so slightly.

Currently built-in:

filter(callbackfn: (value: T, index: number, array: Array<T>) => any, thisArg?: any): Array<T>;

My version, only adding <T> just after the function name and leaving the rest unchanged:

filter<T>(callbackfn: (value: T, index: number, array: Array<T>) => any, thisArg?: any): Array<T>;

I then rewrote OPs example, merely adding a type hint to the filter call:

type Tests = Array<number | string>;

function testFn(tests: Tests) {
    tests
    .filter<number>(test => typeof test !== 'string')
    .map(test => Math.max(test, 10));
}

and it works. If I change the type hint to "string" Flow complains again. So the type hint is accepted.


I don't know what having/adding a generic parameter to the function name actually does inside Flow, vs. only using it deeper inside a function definition (what is clear, also from trying to give a hint when you don't have the generic type on the function name is that functions without it cannot be called with such a hint, you would get error Cannot call non-polymorphic function type with type arguments).

Maybe it would be helpful to add it to all those functions so that they can take such hints? A feature that also isn't documented anywhere I think. Maybe someone like @wchargin or @jbrown215 would be able to say something about what I did here?

@lll000111 your solution works for me, thanks!

@lll000111: Thanks for the ping.

Unfortunately, the type for filter that you’ve provided isn’t sound.
With your code, I can write the following:

function isEven(n: number): boolean {
  if (typeof n !== "number") {
    console.log("impossible(?): " + (n: empty));
    return n.thisTypechecksButIsNotValid();
  }
  return n % 2 === 0;
}

const strings: Array<string> = ["one", "two"];
strings.filter<number>(isEven);  // type-checks (it shouldn't!) and throws

The monomorphized filter<number> has type

filter<number>(
  callbackfn: (value: number, index: number, array: Array<number>) => any,
  thisArg?: any,
): Array<number>;

the call to filter typechecks, but of course this fails at runtime. We
need the “T” in the definition of filter to refer to _the element
type of the array_, not some arbitrary type.

Note also that your example

type Tests = Array<number | string>;

function testFn(tests: Tests) {
    tests
    .filter<number>(test => typeof test !== 'string')
    .map(test => Math.max(test, 10));
}

also typechecks if you change the !== to ===, so clearly this isn’t
checking the types as you might expect!

I don't know what having/adding a generic parameter to the function
name actually does inside Flow,

It shadows the outer type parameter on the class (Array<T>).

If we were in a functional language like OCaml or Haskell, we’d write
something like the following:

type Tests = Array<number | string>;

function maybeNumber(test: number | string): number[] {
  return typeof test === "number" ? [test] : [];
}

function testFn(tests: Tests) {
  tests                // Array<number | string>
    .map(maybeNumber)  // Array<Array<number>>
    .flat();           // Array<number>
}

Of course, OCaml and Haskell each have a first-class filter_map for
the built-in list/array/etc. types, but this demonstrates how you can
achieve the same effect given map and flat, which many data
structures support.

You can of course implement the same pattern in Flow, though it may feel
less familiar to JavaScript developers.

Does this make sense?

@wchargin I know/knew that this does not do any type checking. My solution is a hack around Flow's inability to do any type checking with filter. Since Flow does not do it, apart from the special hard-coded filter(Boolean) (which by the way cannot handle actual Boolean values and also fails for numbers because of 0), it's kind of pointless — whatever I specify is useless, I just cannot use filter at all. Or I use a typecast through any. Or I use my hack, which as far as Flow hacks go still is pretty clean.

Of course the responsibility fully lies with the author of the code to provide the correct type hint. But that is the case for any any even more so, with much more severe side effects (if it's just a plain any.

So what is the way to filter in flow? How can I type this simple as hell function??

const whatever = (benefits: Benefit[]) =>
  benefits.filter<Benefit>((b) => Boolean(b.basedOnGrossPay)

I'm getting this cryptic error:

((
  callbackfn: typeof Boolean
) => Array<$NonMaybeType<Benefit>>) &
  ((
    callbackfn: (
      value: Benefit,
      index: number,
      array: Array<Benefit>
    ) => mixed,
    thisArg?: any
  ) => Array<Benefit>)
Cannot call `benefits.filter` because: Either cannot call non-polymorphic  function type [1] with type arguments. Or cannot call non-polymorphic  function type [2] with type arguments.Flow(InferError)
core.js(263, 5): [1] function type
core.js(264, 5): [2] function type

@danielo515: Just benefits.filter((b) => b.basedOnGrossPay) should
work, presuming that basedOnGrossPay is a field on the Benefit type.

The error is telling you what’s going on: filter is not a polymorphic
function, so your attempt to call filter<Benefit>(...) is ill typed.

Yes, you're right, the main problem is that filter is not a polymorphic function. I didn't understood the first error flow gave me correctly and I thought I was missing that annotation.
What actually made the typing work (or relax down, not now) is to type the return value of the function to be Benefit[].
So the working thing is:

const whatever = (benefits: Benefit[]): Benefit[] =>
  benefits.filter((b) => Boolean(b.basedOnGrossPay)

You shouldn’t need the Boolean coercion, either.

You shouldn’t need the Boolean coercion, either.

Even if the property I'm returning is not a boolean? I'll try that out.

Correct. It’s there in the type: the callbackfn may return mixed.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jamiebuilds picture jamiebuilds  Â·  3Comments

mjj2000 picture mjj2000  Â·  3Comments

john-gold picture john-gold  Â·  3Comments

bennoleslie picture bennoleslie  Â·  3Comments

iamchenxin picture iamchenxin  Â·  3Comments