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の神々からの助けをいただければ幸いです。
現在、これを正しく入力するための最大のハードルは、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
これらのタイプは私には正しいように見えます。
もちろん、それはcurry
のimplementationがその型で満足できることを意味するものではありません。あなたはそれを次のように実装できるかもしれません:
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 を提出しました。
乾杯!
ここでの最大の問題は、可変数の「カレーレベル」でジェネリック関数を定義しようとしていることです。 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>
型を定義するときにこのレベルの再帰的フロー制御をサポートする型システムはほとんどないため、多くの言語ではこれらの組み込み型のような型が展開されています。
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
呼び出し。ただし、部分的に適用された関数を正しく複数回呼び出すことができるように注意する必要があります。