web-dev-qa-db-ja.com

TypeScript 3の汎用カレー関数

TypeScript 3.0が導入されました 一般的な残りのパラメーター

この時点まで、curry関数には、TypeScriptで 有限数の関数オーバーロード と、実装内で渡された引数の数を問い合わせる一連の条件ステートメントを注釈として付ける必要がありました。

汎用的なレストパラメーターが、完全に汎用的なソリューションを実装するために必要なメカニズムを最終的に提供することを期待しています。

この新しい言語機能を使用して一般的なcurry関数を作成する方法を知りたい...もちろんそれが可能であると仮定します!

hackernoonで見つけたソリューション から少し変更した残りのパラメーターを使用したJS実装は、次のようになります。

function curry(fn) {
  return (...args) => {
    if (args.length === 0) {
      throw new Error("Empty invocation")
    } else if (args.length < fn.length) {
      return curry(fn.bind(null, ...args))
    } else {
      return fn(...args)
    }
  }
}

汎用的な残りのパラメーターと関数のオーバーロードを使用して、TypeScriptでこのcurry関数に注釈を付けようとすると、次のようになります。

interface CurriedFunction<T extends any[], R> {
  (...args: T): void // Function that throws error when zero args are passed
  (...args: T): CurriedFunction<T, R> // Partially applied function
  (...args: T): R // Fully applied function
}

function curry<T extends any[], R>(
  fn: CurriedFunction<T, R>
): CurriedFunction<T, R> {
  return (...args: T) => {
    if (args.length === 0) {
      throw new Error("Empty invocation")
    } else if (args.length < fn.length) {
      return curry(fn.bind(null, ...args))
    } else {
      return fn(...args)
    }
  }
}

ただし、TypeScriptはエラーをスローします。

Type 'CurriedFunction<any[], {}>' is not assignable to type 'CurriedFunction<T, R>'.
Type '{}' is not assignable to type 'R'.

R{}と推定される場所と理由がわかりません。

TypeScriptの神々からの助けをいただければ幸いです。

11
wagerfield

現在、これを正しく入力するための最大のハードルは、TypeScript 3.0でTypeScriptがタプルを連結または分割できないことです。これを行うための suggestions があり、TypeScript 3.1以降のバージョンで何かが動作している可能性がありますが、現在はありません。今日でできることは、最大有限長までのケースを列挙するか、または コンパイラに再帰を使用するように仕向ける非推奨 です。

タプルと長さを取り、その長さでタプルを初期コンポーネントと残りに分割できるTupleSplit<T extends any[], L extends number>タイプの関数があると想像すると、TupleSplit<[string, number, boolean], 2>{init: [string, number], rest: [boolean]}を生成するように、curry関数のタイプを何かとして宣言できますこのような:

declare function curry<A extends any[], R>(
  f: (...args: A) => R
): <L extends TupleSplit<A, number>['init']>(
    ...args: L
  ) => 0 extends L['length'] ?
    never :
    ((...args: TupleSplit<A, L['length']>['rest']) => R) extends infer F ?
    F extends () => any ? R : F : never;

それを試すことができるように、LからTupleSplit<T, L>まで(必要に応じて追加できます)でのみ機能する3のバージョンを紹介しましょう。次のようになります。

type TupleSplit<T extends any[], L extends number, F = (...a: T) => void> = [
  { init: [], rest: T },
  F extends ((a: infer A, ...z: infer Z) => void) ?
  { init: [A], rest: Z } : never,
  F extends ((a: infer A, b: infer B, ...z: infer Z) => void) ?
  { init: [A, B], rest: Z } : never,
  F extends ((a: infer A, b: infer B, c: infer C, ...z: infer Z) => void) ?
  { init: [A, B, C], rest: Z } : never,
  // etc etc for tuples of length 4 and greater
  ...{ init: T, rest: [] }[]
][L];

これで、次のような関数でcurryの宣言をテストできます

function add(x: number, y: number) {
  return x + y;
}
const curriedAdd = curry(add);

const addTwo = curriedAdd(2); // (y: number) => number;
const four = curriedAdd(2,2); // number
const willBeAnError = curriedAdd(); // never

これらのタイプは私には正しいように見えます。


もちろん、それはcurryimplementationがその型で満足できることを意味するものではありません。あなたはそれを次のように実装できるかもしれません:

return <L extends TupleSplit<A, number>['init']>(...args: TupleSplit<A, L['length']>['rest']) => {
  if (args.length === 0) {
    throw new Error("Empty invocation")
  } else if (args.length < f.length) {
    return curry(f.bind(null, ...args))
  } else {
    return f(...args as A)
  }
}

たぶん。私はそれをテストしていません。

とにかく、それが理にかなっていて、あなたにいくつかの方向性を与えることを願っています。幸運を!


更新

すべての引数を渡さない場合、curry()がさらにカリー化された関数を返すという事実には注意を払いませんでした。これを行うには、次のような再帰型が必要です。

type Curried<A extends any[], R> =
  <L extends TupleSplit<A, number>['init']>(...args: L) =>
    0 extends L['length'] ? never :
    0 extends TupleSplit<A, L['length']>['rest']['length'] ? R :
    Curried<TupleSplit<A,L['length']>['rest'], R>;

declare function curry<A extends any[], R>(f: (...args: A)=>R): Curried<A, R>;

function add(x: number, y: number) {
  return x + y;
}
const curriedAdd = curry(add);

const addTwo = curriedAdd(2); // Curried<[number], number>
const three = addTwo(1); // number
const four = curriedAdd(2,2); // number
const willBeAnError = curriedAdd(); // never

それは元の定義に似ています。


しかし、これを実行すると、

const wat = curriedAdd("no error?"); // never

エラーが発生する代わりに、neverが返されます。これはコンパイラのバグのようですが、まだフォローアップしていません。編集:さて、私はこれについて Microsoft/TypeScript#26491 を提出しました。

乾杯!

6
jcalz

ここでの最大の問題は、可変数の「カレーレベル」でジェネリック関数を定義しようとしていることです。 a => b => c => dまたはx => y => zまたは(k, l) => (m, n) => o、これらの関数はすべて同じように(ジェネリックではありますが)同じ型定義F<T, R>-任意に分割できないため、TypeScriptでは不可能なことgeneric rests 2つの小さなタプルに...

概念的には次のものが必要です。

FN<A extends any[], R> = (...a: A) => R | (...p: A.Prefix) => FN<A.Suffix, R>

TypeScript AFAIKはこれを行うことができません。

あなたの最善の策は、いくつかの素敵なオーバーロードを使用することです:

FN1<A, R>             = (a: A) => R
FN2<A, B, R>          = ((a: A, b: B) => R)             | ((a: A) => FN1<B, R>)
FN3<A, B, C, R>       = ((a: A, b: B, c: C) => R)       | ((a: A, b: B) => FN1<C, R>)       | ((a: A) => FN2<B, C, R>)
FN4<A, B, C, D, R>    = ((a: A, b: B, c: C, d: D) => R) | ((a: A, b: B, c: C) => FN1<D, R>) | ((a: A, b: B) => FN2<C, D, R>) | ((a: A) => FN3<B, C, D, R>)

function curry<A, R>(fn: (A) => R): FN1<A, R>
function curry<A, B, R>(fn: (A, B) => R): FN2<A, B, R>
function curry<A, B, C, R>(fn: (A, B, C) => R): FN3<A, B, C, R>
function curry<A, B, C, D, R>(fn: (A, B, C, D) => R): FN4<A, B, C, D, R>

型を定義するときにこのレベルの再帰的フロー制御をサポートする型システムはほとんどないため、多くの言語ではこれらの組み込み型のような型が展開されています。

1

TypeScriptの現在のバージョンでは、比較的単純で正しく型付けされたジェネリックカレー関数を作成できます。

type CurryFirst<T> = T extends (x: infer U, ...rest: any) => any ? U : never;
type CurryRest<T> =
    T extends (x: infer U) => infer V ? U :
    T extends (x: infer U, ...rest: infer V) => infer W ? Curried<(...args: V) => W> :
    never

type Curried<T extends (...args: any) => any> = (x: CurryFirst<T>) => CurryRest<T>

const curry = <T extends (...args: any) => any>(fn: T): Curried<T> => {
    if (!fn.length) { return fn(); }
    return (arg: CurryFirst<T>): CurryRest<T> => {
        return curry(fn.bind(null, arg) as any) as any;
    };
}

describe("Curry", () => {
    it("Works", () => {
        const add = (x: number, y: number, z: number) => x + y + z;
        const result = curry(add)(1)(2)(3)
        result.should.equal(6);
    });
});

これは、2つの型コンストラクターに基づいています。

  • CurryFirstは、関数が与えられると、その関数の最初の引数のタイプを返します。
  • CurryRestは、最初の引数が適用されたカレー関数の戻りの型を返します。特殊なケースは、関数型Tが引数を1つだけ取り、その後CurryRest<T>は、関数タイプTの戻り値のタイプを返すだけです。

これら2つに基づいて、タイプTのカリー化された関数のタイプシグネチャは次のようになります。

Curried<T> = (arg: CurryFirst<T>) => CurryRest<T>

ここにいくつかの簡単な制約を加えました:

  • パラメータなしの関数をカレーしたくない。簡単に追加できますが、意味がないと思います。
  • thisポインタは保持しません。純粋なFPここに着陸するため、これは私には意味がありません。

カレー関数が配列内のパラメーターを蓄積し、1つのfn.apply、複数ではなくfn.bind呼び出し。ただし、部分的に適用された関数を正しく複数回呼び出すことができるように注意する必要があります。

0
Pete