Adam
Adam

Reputation: 2287

Trouble exporting curried generic function

I have a library of helper functions that I want to export curried versions of.

A small chunk of it looks like this:

export function curry2<A,B,C>(f: (x: A, y: B) => C): (x: A) => (y: B) => C {
  return (a) => (b) => f(a, b)
}

function _push<A>(item: A, items: Array<A>): Array<A> {
  return items.concat(item)
}

export const push = curry2(push)

But that doesn't work. Flow complains expression curry2(push), saying:

- type parameter 'A' of function call. Missing annotation.
- type parameter 'B' of function call. Missing annotation.

So I tried to fix this by annotating the exported identifier:

export const push<A>: (item: A) => (items: Array<A>) => Array<A>

But this doesn't work because const expressions can't introduce generic type variables.

So I figured I would have to export an actual function in order to be able to annotate it:

export function push<A> (item: A): (items: Array<A>) => Array<A> {
  return curry2(_push)(item);
}

But at this point I'm basically going to re-write a big chunk of curry for each function I want to export.

Is there a better way to help Flow fill in the generic type variables of exports in const expressions?

Upvotes: 0

Views: 623

Answers (3)

aij
aij

Reputation: 6481

Type inference with subtyping, polymorphism, and compact types has traditionally been problematic. There's a recent paper describing a modification to Hindley-Milner inference which might be useful if it could be adapted to Flow.

Anyway, Flow does actually seem to allow some polymorphic const values. For example

// @flow strict

const id: <T>(T) => T = <T>(a: T):T => a;
const genericConstant: <T>(T) => T = id(id);

const dup: <U>(U) => [U, U] = <U>(a: U) => [a, a];
const iddup: <T>(T) => [T, T] = id(dup);
const dupdup: [<T>(T) => [T, T], <U>(U) => [U, U]] = dup(dup);

const curry2 = <A,B,C>(f: (A, B) => C) => (a: A) => (b: B): C => f(a, b);

function _push<A>(item: A, items: Array<A>): Array<A> {
  return items.concat(item)
}

const push2 /*broken : <A>(A) => Array<A> => Array<A> */ = curry2(_push)

const pair: <A,B>(A, B) => [A, B] = <A, B>(a: A, b: B): [A, B] => [a, b]

const pair2 /*broken: <A,B>(A) => (B) => [A, B]*/ = curry2(pair)

I'm not sure what rule it is following, but it's definitely different from SML's value restriction.

Upvotes: 0

Avik Chaudhuri
Avik Chaudhuri

Reputation: 235

See my answer to the same question here: https://github.com/facebook/flow/issues/2165#issuecomment-236868389

The main limitation here is that Flow doesn't infer polymorphic types at all. In particular, whenever it sees a call to a polymorphic function, it immediately instantiates the type parameters with fresh type arguments, and the result is never "generalized" back the way Hindley-Milner systems (https://en.wikipedia.org/wiki/Hindley%E2%80%93Milner_type_system) do.

The reason for this limitation is that this kind of polymorphic type inference is undecidable with subtyping (see Pierce, "Bounded quantification is undecidable", POPL'92), and subtyping is a necessary feature for JavaScript (but not so much for a ML-like language).

Upvotes: 2

gcanti
gcanti

Reputation: 1462

I suspect is not possible to define a curry2 function without higher kinded types

type Function2<A, B, C> = (a: A, b: B) => C;
type CurriedFunction2<A, B, C> = (a: A) => (b: B) => C;

export function curry2<A, B, C>(f: Function2<A, B, C>): CurriedFunction2<A, B, C> {
  return (a: A) => (b: B): C => f(a, b)
}

export const push = curry2(function <A>(item: A, items: Array<A>): Array<A> {
  return items.concat(item)
})

The last definition means that push has type

push: CurriedFunction2<A, Array<A>, Array<A>> // <= higher kinded type?

Upvotes: 0

Related Questions