Что за страшный зверь этот ваш карринг? Говорим о набирающем популярность TypeScript, рассказываем и показываем на нем же.
Ты научишься создавать типы для карринга и Ramda. Для следования этому гайду желателен опыт работы с примитивными типами TypeScript. К концу ты узнаешь, как создавать мощные типы вроде этого:
Карринг, что ты такое?
Карринг или каррирование – это процесс преобразования функции, которая принимает несколько аргументов, в серию функций, которые принимают один аргумент за раз.
Вот функция, которая принимает два числа и возвращает их сумму:
Каррированная версия simpleAdd
выглядит так:
В этом руководстве сначала рассмотрим, как создавать типы TypeScript, которые работают со стандартной реализацией карринга.
Затем мы будем развивать их в продвинутые типы, что позволяют каррированным функциям принимать 0 или более аргументов.
Карринг v0
Каррированная функция:
Наш первый тип карринга принимает кортеж параметров P
и возвращаемый тип R
. Это тип рекурсивной функции, который зависит от длины P
:
Если HasTail
сообщает false
– все параметры использованы и пришло время вернуть тип R
из оригинальной функции. Иначе, если остаются параметры для использования, мы выполняем рекурсию внутри типа. CurryV0
описывает функцию с возвращаемым типом CurryV0
, пока существует Tail
(HasTail<P> extends true
).
Вот доказательство без какой-либо реализации:
Представим рекурсию выше:
Подсказки типов работают для неограниченного количества параметров:
Карринг v1
Мы забыли обработать сценарий, в котором передаются оставшиеся параметры:
Мы попытались использовать оставшиеся параметры, но это не сработает, так как ожидается один параметр или аргумент, который мы назвали arg0
. Так, нужно получить хотя бы один аргумент arg0
и любые дополнительные (необязательные) аргументы внутри оставшегося параметра rest
. Включаем остальные параметры с помощью Tail
и Partial
:
Проверка:
Ужасная ошибка! Аргументы обрабатываются очень плохо, а TS молчит :(
Это проблема проектирования, которая возникает из-за единственного принимаемого аргумента arg0
. Нужно отслеживать аргументы, которые используются одновременно. Избавимся от arg0
и начнем отслеживать используемые параметры:
Но теперь мы потеряли проверку типов, потому что указали отслеживание любых []
параметров. Но дело не только в этом. Теперь использование Tail
бессмысленно, потому что Tail
работает, когда принимается один аргумент за раз.
Нужно больше инструментов!
Рекурсивные типы
Рассмотрим инструменты для определения параметров. Отслеживая используемые параметры с помощью T
, мы сможем угадать оставшиеся параметры.
Пристегните ремни! Очередная мощная техника прямо по курсу:
Last
Этот тип принимает кортеж в качестве параметра и извлекает последнюю запись:
Давайте проверим:
Основные инструменты №1
Где мы? Нам нужны инструменты для отслеживания аргументов, помните? А значит, нужно знать, какие типы параметров можно использовать, какие из них были использованы, и какие будут следующими. Приступим!
Length
Для анализа выше нужно выполнить итерации по кортежам. В TypeScript 3.4.x нет аналога for. В идеале нужен счетчик:
Проверяем:
Наполняя кортеж типом any
, мы создали нечто похожее на переменную, которую можно увеличивать. Length
просто задает размер кортежа и работает с любым другим типом кортежа:
Prepend
Prepend
добавляет тип E
поверх кортежа T
:
Проверка:
В примере с Length
мы увеличивали счетчик вручную. Prepend
– идеален как основа для счетчика. Вот так он работает:
Drop
Drop принимает кортеж T
и удаляет первые N
записей. Для этого используем те же приемы, что и в Last
:
Проверка:
Drop
будет повторяться до тех пор, пока Length<I>
не совпадет со значением N
, которое мы передали. Другими словами, тип индекса 0
выбирается условным методом доступа до тех пор, пока это условие не будет выполнено. + мы использовали Prepend
, чтобы увеличить счетчик, как в цикле. Так что Length<I>
используется в качестве счетчика рекурсии, и это способ свободной итерации в TS.
Карринг v2
Ты проделал нелегкий путь, добравшись сюда, и это здорово!
Предположим, теперь мы можем отследить, как 2 параметра используются каррированной функцией:
С Drop
узнаем количество употребленных и не использованных параметров:
Обновим предыдущую версию со сломанным Tail
:
Что же мы сделали?
Во-первых, Drop<Length<T>, P>
означает удаление употребленных параметров. Затем, если длина Drop<Length<T>, P>
не равна 0
, тип карринга должен продолжать рекурсию с отброшенными параметрами, пока... Наконец, когда все параметры употреблены, Length
отброшенных параметров равна 0
, а возвращаемый тип – R
.
Карринг v3
Есть еще одна ошибка выше: TS жалуется, что Drop
не относится к типу []
. Иногда TS жалуется на неожиданный тип, несмотря на то, что он подходит! Это повод добавить еще один инструмент в коллекцию:
Cast
Cast
требует, чтобы TS перепроверил тип X
с типом Y
, и тип Y
будет применен только в случае неудачи. Так можно предотвратить жалобы TS:
Проверка:
И вот предыдущий карринг без каких-либо жалоб:
Проверка:
Карринг v4
Мы все еще не можем взять оставшиеся параметры. И вот почему:
Поскольку количество оставшихся параметров может быть неограниченными, 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
:
Проверка:
Карринг v5
Вот и готовы инструменты для типа каррирования. Gaps
– это новая замена Partial
, а GapsOf
заменит Drop
:
Проверим:
Для проверки принудительно установим значения, которые будут взяты с каррированной функцией:
Упс! Маленькая проблема. Дело в том, что мы «опередили» Ramda! Наш тип понимает сложные использования плейсхолдеров. Другими словами, плейсхолдеры Ramda просто не работают с оставшимися параметрами:
Несмотря на то, что все все выглядит правильно, мы получим падение. Реализация карринга Ramda не справляется с комбинацией плейсхолдеров и оставшихся параметров.
Карринг
Осталось решить последнюю проблему с подсказками параметров. Полезно знать названия параметров, с которыми работаешь. Версия выше не допускает такого рода подсказок. Вот исправление:
Мы получили подсказки для Visual Studio Code. Как? Да просто заменили типы параметров P
иR
, которые использовались для обозначения типов параметра и возврата. Вместо этого мы использовали тип функции F
, из которого извлекли эквивалент Parameters<F>
и R
с ReturnType<F>
. Так, TypeScript способен сохранять имя параметров даже после каррирования:
Один нюанс: при использовании пробелов имя параметра теряется.
Комментарии