برنامهنویسی تابعی - چگونه در تایپاسکریپت ۴ تابع curry و compose بنویسیم؟


بعد از بررسی انواع متغیر چندگانه، میخواستم این را بسازم، اما میخواهم بدانم چگونه از آرایهای از توابع استفاده کنم.

این اولین تلاش من است:

function curry<T extends any[]>(fn: (...args: T) => any) {
  return function(...args: T) {
    return args.length >= fn.length
      ? fn(...args)
      : curry(fn.bind(undefined, ...args));
  }
}

اما برای fn.bind من دریافت میکنم «زمینهٔ 'this' از نوع '(...args: T) => any' قابل انتساب به 'this' متد از نوع '(this: undefined, ...args: any[]) => any' نیست.»

نظری دارید؟

پاسخ شماره ۱

شما واقعاً در کد خود از نوعهای تاپل متغیر استفاده نمیکنید. نوع خاصی از curry() که شما پیادهسازی میکنید، آن است که یک لیست جزئی از پارامترها را میگیرد و سپس ممکن است یک تابع دیگر بازگرداند که باقیمانده جزئی لیست را میگیرد. این به این معنی است که تاپل اولیه پارامترها T ممکن است به قطعات مختلفی تقسیم شود، و بنابراین این به طور خودکار از زمینه استنتاج نخواهد شد همانطور که تابع partialCall() در مستندات میتواند.

در عوض، شما نیاز دارید که بهوضوح تاپلهای T را به زیرتاپلهای ممکن جدا کنید. بیایید نوع خروجی مورد نظر از curry() را که تابعی میگیرد که پارامترهای آن تاپل A هستند و نوع بازگشتی آن R است، نمایش دهیم:

type Curried<A extends any[], R> =
  <P extends Partial<A>>(...args: P) => P extends A ? R :
    A extends [...SameLength<P>, ...infer S] ? S extends any[] ? Curried<S, R>
    : never : never;

type SameLength<T extends any[]> = Extract<{ [K in keyof T]: any }, any[]>

بگذارید آن را به انگلیسی ترجمه کنم. یک Curried<A, R> یک تابع عمومی است که آرگومانهای آن باید از نوع تاپل P باشند که محدود به Partial<A> است.

برای تاپلها، Partial<A> در نهایت به این معنی است که میتوانید هر پسوندی از تاپل را حذف کنید (از جایی تا انتها). بنابراین [1, 2, 3] قابل انتساب به Partial<[1,2,3,4,5,6,7]> است، اما [1, 2, 4] نیست. با undefined کمی پیچیدگی وجود دارد، زیرا [1, undefined, 3] نیز قابل انتساب به Partial<[1,2,3,4,5,6,7]> است، اما من این را نادیده میگیرم و اگر اهمیت پیدا کند، میتوان دور آن راهکاری یافت. به هر حال، این بدان معناست که آرگومانهای Curried<A, R> باید بخشی از پیشوند (قطعه اولیه) تاپل A باشند.

نوع بازگشتی Curried<A, R> به پیشوند P که وارد میشود بستگی دارد. اگر P کل زوج A باشد، نوع بازگشتی فقط R است (این همان چیزی است که وقتی همه آرگومانهای تابع را نهایتاً دادهاید اتفاق میافتد). در غیر این صورت، A را به پیشوند P و پسوند آن S تقسیم میکنید، و یک تابع کرری جدید از نوع Curried<S, R> بازمیگردانید.

تقسیم A به [...SameLength<P>, ...infer S] از نوعهای تاپل متغیر استفاده میکند. توجه داشته باشید که SameLength<P> تنها یک تاپل به طول P است اما نوع عناصر آن any است. این از مشکلی جلوگیری میکند که در آن P به شکل بسیار محدود استنباط میشود (مثلاً A برابر [number, number, number] است و سپس P برابر [0, 0] میشود). نمیتوانید [number, number, string] را به [0, 0, ...infer S] تقسیم کنید زیرا number قابل انتساب به 0 نیست. اما تنها چیزی که برای ما اهمیت دارد طول است، بنابراین [number, number, string] را به [any, any, ...infer S] تقسیم میکنیم و این کار میکند و S به صورت string استنباط میشود.

خب، از آن استفاده کرده و آن را پیادهسازی کن:

function curry<A extends any[], R>(fn: (...args: A) => R): Curried<A, R> {
  return (...args: any[]): any =>
    args.length >= fn.length ? fn(...args as any) : curry((fn as any).bind(undefined, ...args));
}

من در پیادهسازی curry() از تعداد زیادی تأیید نوع استفاده کردم، چون کامپایلر تقریباً در تلاش برای بررسی اینکه تابع بازگشتی قابل انتساب به Curried<A, R> است، بهطور کامل گیج میشود. به جای اینکه نگران آن باشم، فقط به کامپایلر میگویم که بررسی ایمنی را انجام ندهد و خودم مسئولیت درست بودن پیادهسازی را بر عهده میگیرم. (اگر اشتباه باشد، خودم را مقصر میدانم، نه کامپایلر).

خوب، پس این هم شد. کار میکند؟

const fn = (a: string, b: number, c: boolean) => (a.length <= b) === c ? "yep" : "nope";

const cFn = curry(fn);
const val1 = cFn("")(1)(true);
console.log(val1); // yep

const val2 = cFn("", 1, true);
console.log(val2); // yep

const val3 = cFn()()()()("", 1)()()(true); // yep

به نظر من خوب است. توجه داشته باشید که طبق تعریف من اگر شما یک Curried<A, R> را بدون هیچ آرگومانی فراخوانی کنید، فقط یک Curried<A, R> باز میگردد. در اینجا چند خطای عمدی آورده شده است تا بتوانید ببینید که کامپایلر آنها را شناسایی میکند:

// errors
cFn(1, 1, true); // error!
//  ~ <-- not a string
cFn("", 1, true, false); // error!
//               ~~~~~ <-- Expected 0-3 arguments, but got 4
cFn("")(1)(false)(true); // error!
//~~~~~~~~~~~~~~~ <-- This expression is not callable.

اینها به نظر من خطاهای صحیح میآیند.


لینک زمین بازی به کد