Обои рабочего стола)
Обои рабочего стола)

Про Змейку

В начале 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)


  1. funca
    00.00.0000 00:00
    +1

    Больше похоже на partial application, чем curry. //зануда мод


  1. 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>;