Про Змейку
В начале 2022 года Змейка (Snake on TS) была ещё Snake on JS. Но прогресс не стоит на месте, и было принято решение, освоить TypeScript и избавить Змейку от any. Никаких сверхъестественных типов там нет, да и речь не о ней. Но поиграть можете :)
Не говорите, что это hard
В репозитории type-challenges каррирование находится в разделе hard,
но мне захотелось реализовать этот тип ещё до того как я это узнал. Начнём.
Для начала напишем саму функцию curry
function curry<Fn extends Func<any, any>>(func: Fn) {
return function _curry(...args: Array<any>) {
if (args.length === func.length) {
return func(...args);
}
return function (...args2: Array<any>) {
return _curry(...[...args, ...args2]);
};
};
}
получилось вот это. Как результат: корми сколько угодно параметров и какие угодно.
Теперь приступим к типу Curry.
В первой итерации получаем:
type Curry<
Fn extends (...args: Array<any>) => any,
Params extends Parameters<Fn>[number][] = []
> = Fn extends (...args: infer FnParams) => infer Return
? Params['length'] extends FnParams['length']
? Return
: <Args extends Array<any>>(...args: Args) => [...Params, ...Args]['length'] extends FnParams['length']
? Return
: <Args2 extends Array<any>>(...args: Args2) => Curry<Fn, [...Args, ...Args2]>
: never
На входе функция Fn, с которой мы и будем работать внутри типа.
Params - нужны для отслеживания аргументов функции, если они не были переданы все сразу.
Через infer получаем FnParams и Return нашей функции Fn - так удобнее потом будет работать с ними.
Делаем проверку равенства количества Params и FnParams, если равны "делаем" Return.
Если не равны, реализуем curry в типовом варианте. Возвращаем функцию, в которой проверяем длину Args, если длина равна длине аргументов Fn, то возвращаем Return, если нет - возвращаем функцию, которая принимает оставшиеся аргументы.
Применим наш тип к функции curry
function curry<Fn extends (...args: Array<any>) => any>(func: Fn) {
return function _curry(...args: Array<any>) {
if (args.length === func.length) {
return func(...args);
}
return function (...args2: Array<any>) {
return _curry(...[...args, ...args2]);
};
} as Curry<Fn>;
}
Вроде всё хорошо и finalSum - это number, но...
Что-то пошло не так. Продолжим наши поиски.
Нужно связать параметры нашей Fn с Args и Args2. Этим мы и займёмся.
type Curry<
Fn extends (...args: Array<any>) => any,
Params extends Parameters<Fn>[number][] = []
> = Fn extends (...args: infer FnParams) => infer Return
? Params['length'] extends FnParams['length']
? Return
: <Args extends ParamsSlice<FnParams, Params>>(...args: Args) => [...Params, ...Args]['length'] extends FnParams['length']
? Return
: <Args2 extends ParamsSlice<FnParams, [...Params, ...Args]>>(...args: Args2) => Curry<Fn, [...Params , ...Args, ...Args2]>
: never
Написали вспомогательный тип ParamsSlice
type ParamsSlice<
FnParams extends Array<any>,
Args extends FnParams[number][]
> = FnParams extends [...Args, ...infer Rest]
? Rest extends [infer First, ...infer P]
? [First, ...Partial<P>]
: []
: []
Здесь мы берём оставшиеся аргументы переданной функции и говорим, что первый, из оставшихся, будет обязательным, остальные опциональные.
У нас есть проверка на типы, на количество параметров и, даже, currySum с двумя аргументами возвращает number.
Это однозначно успех. Но сколько аргументов принимает firstNumValid? Давайте проверим.
Второй аргумент sum потерялся. Будем искать.
Args2 extends ParamsSlice<FnParams, [...Params, ...Args]>
Из ParamsSlice возвращается пустой массив. Выясним почему так происходит?
type ParamsSlice<
FnParams extends Array<any>,
Args extends FnParams[number][]
> = FnParams extends [...Args, ...infer Rest]
? Rest extends [infer First, ...infer P]
? [First, ...Partial<P>]
: []
: [FnParams, Args]
Для отладки вернём из ParamsSlice переданные типы: FnParams и Args.
А теперь проверим как работает
FnParams extends [...Args, ...infer Rest]
В нашем случае, мы проверяем a extends 4
, где a - number и результат отрицательный. Проверим это, написав простой тип:
Вывод: нужно приводить наши Args из ParamsSlice к примитивам. То есть сделаем из 4 -number.
type ToPrimitive<T> = T extends number
? number
: T extends string
? string
: T extends boolean
? boolean
: T extends bigint
? bigint
: T extends symbol
? symbol
: {
[Key in keyof T]: T[Key];
};
Написали такой helper.
Проверяем.
Отлично. Теперь напишем тип MapPrimitive для наших Args:
type MapPrimitive<
Arr extends any[],
Res extends any[] = []
> = Arr extends []
? Res
: Arr extends [infer First, ...infer Rest]
? Rest extends any[]
? MapPrimitive<Rest, [...Res, ToPrimitive<First>]>
: never
: never
Просто проходим по всему массиву и применяем к каждому элементу ToPrimitive. Проверяем.
Поправим наш тип ParamsSlice, добавив MapPrimitive:
type ParamsSlice<
FnParams extends Array<any>,
Args extends FnParams[number][]
> = FnParams extends [...MapPrimitive<Args>, ...infer Rest]
? Rest extends [infer First, ...infer P]
? [First, ...Partial<P>]
: []
: []
Посмотрим на нашу функцию firstNumValid
Функция ожидает один аргумент с типом number и возвращает number. Работает!
Повторим эксперимент, который проводили чуть ранее:
Ну и ещё немного тестов
Тут весь код
type ToPrimitive<T> = T extends number
? number
: T extends string
? string
: T extends boolean
? boolean
: T extends bigint
? bigint
: T extends symbol
? symbol
: {
[Key in keyof T]: T[Key];
};
type MapPrimitive<
Arr extends any[],
Res extends any[] = []
> = Arr extends []
? Res
: Arr extends [infer First, ...infer Rest]
? Rest extends any[]
? MapPrimitive<Rest, [...Res, ToPrimitive<First>]>
: never
: never
type ParamsSlice<
FnParams extends Array<any>,
Args extends FnParams[number][]
> = FnParams extends [...MapPrimitive<Args>, ...infer Rest]
? Rest extends [infer First, ...infer P]
? [First, ...Partial<P>]
: []
: []
type Curry<
Fn extends (...args: Array<any>) => any,
Params extends Parameters<Fn>[number][] = []
> = Fn extends (...args: infer FnParams) => infer Return
? Params['length'] extends FnParams['length']
? Return
: <Args extends ParamsSlice<FnParams, Params>>(...args: Args) => [...Params, ...Args]['length'] extends FnParams['length']
? Return
: <Args2 extends ParamsSlice<FnParams, [...Params, ...Args]>>(...args: Args2) => Curry<Fn, [...Params , ...Args, ...Args2]>
: never
Комментарии (2)
iliazeus
00.00.0000 00:00+2(upd. сначала скопировал ошибочную версию, теперь верная, ссылка на плейграунд
Для начала хочу заметить, что вы решили более сложную задачу, чем просто каррирование :) Оригинал подразумевал превращение функции в "цепочку" функций одного аргумента, т.е.
f(1, 2, 3) -> f(1)(2)(3)
, а вы сделали так, что можно вызывать сразу с несколькими.Тем не менее, даже для этой задачи ваше решение кажется переусложненным. Проще всего, я думаю, было написать рекурсию с явными аккумуляторами A1 и A2, которыми перебирать все такие значения, что
[...A1, ...A2] = Parameters<F>
:type _Head<A extends any[]> = A extends [infer H, ...any[]] ? H : never; type _Tail<A extends any[]> = A extends [any, ...infer T] ? T : never; type _Recurse<F extends (...args: any[]) => any, A1 extends any[] = [], A2 extends any[] = Parameters<F>> = A2 extends [] ? F : F extends (...a: [...A1, ...A2]) => infer R ? ((...a1: A1) => _Recurse<(...a2: A2) => R>) & (_Recurse<F, [...A1, _Head<A2>], _Tail<A2>>) : never; type Curry<F extends (...args: any[]) => any> = _Recurse<F>;
funca
Больше похоже на partial application, чем curry. //зануда мод