Что за страшный зверь этот ваш карринг? Говорим о набирающем популярность TypeScript, рассказываем и показываем на нем же.
Ты научишься создавать типы для карринга и Ramda. Для следования этому гайду желателен опыт работы с примитивными типами TypeScript. К концу ты узнаешь, как создавать мощные типы вроде этого:
Карринг или каррирование – это процесс преобразования функции, которая принимает несколько аргументов, в серию функций, которые принимают один аргумент за раз.
Вот функция, которая принимает два числа и возвращает их сумму:
Каррированная версия simpleAdd выглядит так:
В этом руководстве сначала рассмотрим, как создавать типы TypeScript, которые работают со стандартной реализацией карринга.
Затем мы будем развивать их в продвинутые типы, что позволяют каррированным функциям принимать 0 или более аргументов.
Наш первый тип карринга принимает кортеж параметров P и возвращаемый тип R . Это тип рекурсивной функции, который зависит от длины P :
Если HasTail сообщает false – все параметры использованы и пришло время вернуть тип R из оригинальной функции. Иначе, если остаются параметры для использования, мы выполняем рекурсию внутри типа. CurryV0 описывает функцию с возвращаемым типом CurryV0 , пока существует Tail ( HasTail<P> extends true ).
Вот доказательство без какой-либо реализации:
Представим рекурсию выше:
Подсказки типов работают для неограниченного количества параметров:
Мы забыли обработать сценарий, в котором передаются оставшиеся параметры:
Мы попытались использовать оставшиеся параметры, но это не сработает, так как ожидается один параметр или аргумент, который мы назвали arg0 . Так, нужно получить хотя бы один аргумент arg0 и любые дополнительные (необязательные) аргументы внутри оставшегося параметра rest . Включаем остальные параметры с помощью Tail и Partial :
Ужасная ошибка! Аргументы обрабатываются очень плохо, а TS молчит 🙁
Это проблема проектирования, которая возникает из-за единственного принимаемого аргумента arg0 . Нужно отслеживать аргументы, которые используются одновременно. Избавимся от arg0 и начнем отслеживать используемые параметры:
Но теперь мы потеряли проверку типов, потому что указали отслеживание любых [] параметров. Но дело не только в этом. Теперь использование Tail бессмысленно, потому что Tail работает, когда принимается один аргумент за раз.
Нужно больше инструментов!
Рекурсивные типы
Рассмотрим инструменты для определения параметров. Отслеживая используемые параметры с помощью T , мы сможем угадать оставшиеся параметры.
Пристегните ремни! Очередная мощная техника прямо по курсу:
Этот тип принимает кортеж в качестве параметра и извлекает последнюю запись:
Давайте проверим:
Основные инструменты №1
Где мы? Нам нужны инструменты для отслеживания аргументов, помните? А значит, нужно знать, какие типы параметров можно использовать, какие из них были использованы, и какие будут следующими. Приступим!
Length
Для анализа выше нужно выполнить итерации по кортежам. В TypeScript 3.4.x нет аналога for. В идеале нужен счетчик:
Проверяем:
Наполняя кортеж типом any , мы создали нечто похожее на переменную, которую можно увеличивать. Length просто задает размер кортежа и работает с любым другим типом кортежа:
Prepend
Prepend добавляет тип E поверх кортежа T :
Проверка:
В примере с Length мы увеличивали счетчик вручную. Prepend – идеален как основа для счетчика. Вот так он работает:
Drop принимает кортеж T и удаляет первые N записей. Для этого используем те же приемы, что и в Last :
Проверка:
Drop будет повторяться до тех пор, пока Length<I> не совпадет со значением N , которое мы передали. Другими словами, тип индекса 0 выбирается условным методом доступа до тех пор, пока это условие не будет выполнено. + мы использовали Prepend , чтобы увеличить счетчик, как в цикле. Так что Length<I> используется в качестве счетчика рекурсии, и это способ свободной итерации в TS.
Ты проделал нелегкий путь, добравшись сюда, и это здорово!
Предположим, теперь мы можем отследить, как 2 параметра используются каррированной функцией:
С Drop узнаем количество употребленных и не использованных параметров:
Обновим предыдущую версию со сломанным Tail :
Что же мы сделали?
Во-первых, Drop<Length<T>, P> означает удаление употребленных параметров. Затем, если длина Drop<Length<T>, P> не равна 0 , тип карринга должен продолжать рекурсию с отброшенными параметрами, пока. Наконец, когда все параметры употреблены, Length отброшенных параметров равна 0 , а возвращаемый тип – R .
Есть еще одна ошибка выше: TS жалуется, что Drop не относится к типу [] . Иногда TS жалуется на неожиданный тип, несмотря на то, что он подходит! Это повод добавить еще один инструмент в коллекцию:
Cast требует, чтобы TS перепроверил тип X с типом Y , и тип Y будет применен только в случае неудачи. Так можно предотвратить жалобы TS:
И вот предыдущий карринг без каких-либо жалоб:
Мы все еще не можем взять оставшиеся параметры. И вот почему:
Поскольку количество оставшихся параметров может быть неограниченными, TS полагает, что длина нашего кортежа – это число number . Поэтому нельзя использовать Length при работе с оставшимися параметрами, что не так плохо:
При использовании параметров, Drop<Length<T>,P> может проверять только […any[]] . Благодаря этому мы использовали [any,…any[]] в качестве условия для завершения рекурсии.
Все работает! Теперь у тебя есть универсальный, вариативный тип карринига. Как насчет дальнейших улучшений?
Плейсхолдеры
Предоставим нашему типу способность понимать частичное применение любой комбинации аргументов, в любой позиции. Согласно документации Ramda, мы можем сделать это, используя плейсхолдер _ . В ней говорится, что эти вызовы эквивалентны любой каррированной функции f :
Плейсхолдер или «пробел» – это объект, который абстрагирует факт отсутствия аргумента для передачи в определенный момент. Начнем с определения плейсхолдера. Обратимся напрямую к Ramda:
Мы уже знаем, как выполнять первые итерации типов, увеличивая длину кортежа. Но использование Length и Prepend для нашего типа счетчика не вносит ясности. С этого момента мы будем обращаться к счетчику как к итератору. Вот новые псевдонимы для этого:
Pos (Позиция)
Используйте его для запроса позиции итератора:
Next (+1)
Поднимает позицию итератора:
Prev (-1)
Снижает позицию итератора:
Проверка:
Итератор
Он создает итератор (наш тип счетчика) в позиции, определяемой Index , и может начинать с позиции другого итератора, используя From :
Проверка:
Основные инструменты №2
Отлично, что делаем дальше? Нужно проанализировать передачу плейсхолдера в качестве аргумента. Так мы поймем, был параметр «пропущен» или «отложен». Вот инструменты для этой цели:
Reverse
Reverse даст необходимую свободу. Он принимает кортеж T и превращает его в кортеж R благодаря новым типам итераций:
Проверка:
Concat
И родился из Reverse Concat . Он объединяет кортежи T1 и T2 . Мы делали это в test59 :
Append
С помощью Concat Append может добавить тип E в конец кортежа T :
Вот и готовы инструменты для типа каррирования. Gaps – это новая замена Partial , а GapsOf заменит Drop :
Для проверки принудительно установим значения, которые будут взяты с каррированной функцией:
Упс! Маленькая проблема. Дело в том, что мы «опередили» Ramda! Наш тип понимает сложные использования плейсхолдеров. Другими словами, плейсхолдеры Ramda просто не работают с оставшимися параметрами:
Несмотря на то, что все все выглядит правильно, мы получим падение. Реализация карринга Ramda не справляется с комбинацией плейсхолдеров и оставшихся параметров.
Карринг
Осталось решить последнюю проблему с подсказками параметров. Полезно знать названия параметров, с которыми работаешь. Версия выше не допускает такого рода подсказок. Вот исправление:
Мы получили подсказки для Visual Studio Code. Как? Да просто заменили типы параметров P и R , которые использовались для обозначения типов параметра и возврата. Вместо этого мы использовали тип функции F , из которого извлекли эквивалент Parameters<F> и R с ReturnType<F> . Так, TypeScript способен сохранять имя параметров даже после каррирования: