Узнайте, как создавать шрифты для карри и Ramda

Несмотря на популярность каррирования и рост функционального программирования (и TypeScript), сегодня по-прежнему сложно использовать карри и иметь надлежащие проверки типов. Даже известные библиотеки, такие как Ramda, не предоставляют универсальных типов для своих реализаций карри (но мы будем).

Однако для того, чтобы следовать этому руководству, вам не нужен опыт функционального программирования. Руководство посвящено каррированию, но я предпочитаю только тему, чтобы научить вас продвинутым техникам TypeScript. Вам просто нужно немного попрактиковаться с примитивными типами TypeScript. А к концу этого прохождения вы станете настоящим мастером TS 🧙.

Если вы функциональный программист, вы, вероятно, уже используете каррирование для создания мощных композиций и частичных приложений ... А если вы немного отстали, пора сделать скачок в функциональное программирование, начать уходить от императивной парадигмы и решать проблемы быстрее, с легкостью и обеспечьте возможность повторного использования в вашей кодовой базе.

В конце этого руководства вы узнаете, как создавать мощные типы, например:

На самом деле у Рамды действительно есть какие-то посредственные сорта карри. Эти типы не являются общими, жестко запрограммированными, что ограничивает нас определенным количеством параметров. Начиная с версии 0.26.x, он следует только максимум 6 аргументам и не позволяет нам очень легко использовать его знаменитую функцию заполнителя с TypeScript. Почему? Это сложно, но мы согласны с тем, что с нас достаточно, и мы собираемся это исправить!

Что такое карри?

Но прежде чем мы начнем, давайте убедимся, что у вас есть очень базовые представления о том, что такое каррирование. Каррирование - это процесс преобразования функции, которая принимает несколько аргументов, в серию функций, которые принимают по одному аргументу за раз. Ну это теория.

Я предпочитаю примеры, а не слова, поэтому давайте создадим функцию, которая принимает два числа и возвращает результат их сложения:

Карри-версия simpleAdd будет:

В этом руководстве я сначала объясню, как создавать типы TypeScript, которые работают со стандартной реализацией карри.

Затем мы превратим их в более расширенные типы, которые могут позволить каррированным функциям принимать 0 или более аргументов.

И, наконец, мы сможем использовать «пробелы», которые абстрагируют тот факт, что мы не способны или не хотим приводить аргументы в определенный момент.

TL; DR: мы создадим типы для «классического карри» и «расширенного карри» (Ramda).

Типы кортежей

Прежде чем мы начнем изучать самые продвинутые методы TypeScript, я просто хочу убедиться, что вы знаете кортежи. Типы кортежей позволяют выразить массив, в котором известен тип фиксированного числа элементов. Давайте посмотрим на пример:

Их можно использовать для принудительного применения типа значений внутри массива фиксированного размера:

А также может использоваться в сочетании с остальными параметрами (или деструктуризацией):

Но прежде чем приступить к созданию наших потрясающих видов карри, мы немного разогреемся. Мы собираемся создать первые инструменты, которые нам понадобятся для создания одного из самых основных типов карри. Вперед.

Может быть, вы догадались ... Мы будем много работать с типами кортежей. Мы будем использовать их, как только извлечем параметры из «исходной» каррированной функции. Итак, в качестве примера, давайте создадим базовую функцию:

Мы извлекли типы параметров из fn00 благодаря магии Parameters. Но это не так уж и волшебно, когда вы его перекодируете:

Давайте проверим это:

Хорошо, работает так же, как Parameters. Не бойтесь infer, это одно из самых эффективных ключевых слов для построения типов. Я объясню это более подробно сразу после того, как мы еще немного потренируемся:

Голова

Ранее мы узнали, что функция с «классическим каррированием» принимает по одному аргументу за раз. И мы также увидели, что можем извлекать типы параметров в виде типа кортежа, что очень удобно.
Итак, Head принимает тип кортежа T и возвращает первый тип, который он содержит. Таким образом, мы сможем узнать, какой тип аргумента нужно использовать за раз.

Давайте проверим это:

Хвост

«Классическая каррированная» функция потребляет аргументы один за другим. Это означает, что когда мы использовали Head<Params<F>>, нам каким-то образом нужно перейти к следующему параметру, который еще не был использован. Это цель Tail, она удаляет первую запись, которую может содержать кортеж.

Начиная с TypeScript 3.4, мы не можем «просто» удалить первую запись кортежа. Итак, мы собираемся обойти эту проблему, используя один очень действенный прием:

Используя типы функций, мы смогли указать TypeScript вывести нужный нам кортеж. Если вы еще не поняли, это не проблема, это просто разминка, помните?

Давайте проверим это:

HasTail

Каррированная функция будет возвращать функцию до тех пор, пока все ее параметры не будут использованы. Это условие достигается, когда мы вызываем Tail столько раз, что хвоста не остается, ничего не остается:

Давайте проверим это:

Важные ключевые слова

Вы встретили три важных ключевых слова: type, extends и infer. Они могут сбивать с толку новичков, поэтому передают следующие идеи:

  • extends:
    Чтобы не усложнять, вы можете думать о нем так, как если бы это был наш дорогой старый
    JavaScript ===. Когда вы это сделаете, вы сможете увидеть оператор extends как простой троичный, и тогда его станет намного проще понять. В этом случае extends называется условным типом.
  • type:
    Мне нравится думать о типе как о функции, но только о типах. Он имеет входные данные, которые являются типами (называемыми обобщениями), и имеет выход. Вывод зависит от «логики» типа, и extends - это логический блок, аналогичный предложению if (или тернарному).
  • infer:
    Это увеличительное стекло TypeScript, прекрасного инструмента проверки, который может извлекать типы, заключенные в различные типы структур!

Я думаю, что вы хорошо понимаете и extends, и type, и поэтому мы собираемся немного попрактиковаться с infer. Мы собираемся извлечь типы, которые содержатся внутри разных универсальных типов. Вот как вы это делаете:

Извлечь тип свойства из объекта

Давайте проверим это:

Извлечение внутренних типов из типов функций

Давайте проверим это:

Извлечение универсальных типов из класса или интерфейса

Давайте проверим это:

Извлечение типов из массива

Давайте проверим это:

Извлечь типы из кортежа

Давайте проверим это:

Мы попытались определить тип остальной части кортежа в тип B, но это не сработало должным образом. Это связано с тем, что в TypeScript отсутствует функция, которая позволила бы нам деконструировать один кортеж в другой. Существует активное предложение, которое решает эти проблемы, и вы можете ожидать улучшенных манипуляций с кортежами в будущем. Вот почему Tail устроен именно так.

infer - очень мощный инструмент, который будет вашим основным инструментом для работы с типами.

Карри V0

Разминка 🔥 окончена, и у вас есть знания, чтобы приготовить «классическое карри». Но прежде чем мы начнем, давайте подведем (еще раз), что он должен уметь:

Наш первый тип карри должен принимать кортеж из параметров P и возвращаемого типа R. Это рекурсивный тип функции, различающийся в зависимости от длины P:

Если HasTail сообщает false, это означает, что все параметры были использованы и что пора вернуть тип возврата R из исходной функции. В противном случае остаются параметры для использования, и мы рекурсивно используем наш тип. Рекурсия? Да, CurryV0 описывает функцию, которая имеет тип возврата CurryV0, пока есть Tail (HasTail<P> extends true).

Это очень просто. Вот доказательство без какой-либо реализации:

Но давайте скорее визуализируем рекурсию, которая произошла выше, шаг за шагом:

И, конечно же, подсказки типа работают для неограниченного количества параметров 🎉:

Карри V1

Хорошо, но мы забыли обработать сценарий, в котором мы передаем параметр отдыха:

Мы попытались использовать параметр rest, но это не сработало, потому что на самом деле мы ожидали наличия одного параметра / аргумента, который ранее называли arg0. Итак, мы хотим взять хотя бы один аргумент arg0 и мы хотим получать любые дополнительные (необязательные) аргументы внутри параметра отдыха с именем rest. Давайте включим параметры отдыха, обновив его до Tail & Partial:

Давайте проверим это:

Но мы совершили ужасную ошибку: аргументы расходуются очень плохо. Согласно тому, что мы писали, это не вызовет ни одной ошибки TS:

На самом деле существует большая проблема дизайна, потому что мы сказали, что будем принудительно брать один arg0. Каким-то образом нам нужно будет отслеживать аргументы, которые потребляются за раз. Итак, сначала избавимся от arg0 и начнем отслеживать потребляемые параметры:

Здесь мы использовали ограниченный универсальный шаблон T, который будет отслеживать любые принятые аргументы. Но теперь он полностью сломан, больше нет проверок типов, потому что мы сказали, что хотим отслеживать тип параметров any[] (ограничение). Но не только это, Tail совершенно бесполезен, потому что он работал хорошо только тогда, когда мы принимали один аргумент за раз.

Есть только одно решение: еще инструменты 🔧.

Рекурсивные типы

Следующие инструменты будут использоваться для определения следующих параметров, которые будут использоваться. Как? Отслеживая использованные параметры с помощью T, мы сможем угадать, что осталось.

Пристегните ремень безопасности! Вы собираетесь изучить еще одну мощную технику 🚀:

Последний

Не торопитесь, чтобы попытаться понять этот сложный, но очень короткий тип. Этот
пример принимает кортеж в качестве параметра и извлекает его последнюю запись:

Давайте проверим это:

Этот пример демонстрирует возможности условных типов при использовании в качестве метода доступа индексированного типа. Что? Условный тип, который обращается к внутренним типам типа в режиме командной строки. Для более наглядного объяснения:

Этот метод - идеальный подход и безопасный способ выполнения рекурсии, как мы только что сделали. Но это не только рекурсия, это красивый и наглядный способ организации сложных условных типов.

Основные инструменты # 1

Где мы были? Мы сказали, что нам нужны инструменты для отслеживания аргументов. Это означает, что нам нужно знать, какие типы параметров мы можем использовать, какие из них были использованы, а какие будут следующими. Давайте начнем:

Длина

Чтобы провести упомянутый выше анализ, нам нужно будет перебрать кортежи. В
TypeScript 3.4.x нет такого протокола итераций, который позволял бы нам выполнять итерацию свободно (например, for). Сопоставленные типы могут отображаться от одного типа к другому, но они слишком ограничивают то, что мы хотим сделать. Итак, в идеале мы хотели бы иметь возможность манипулировать каким-то счетчиком:

Давайте проверим это:

Дополнив кортеж any, мы создали нечто похожее на переменную, которую можно увеличивать. Однако Length дает только размер кортежа, поэтому он также работает с любыми другими типами кортежей:

Подготовить

Он добавляет тип E в верх кортежа T, используя наш первый прием TS:

Давайте проверим это:

В примерах Length мы увеличили счетчик вручную. Итак, Prepend - идеальный кандидат на роль базы счетчика. Посмотрим, как это будет работать:

Уронить

Он берет кортеж T и отбрасывает первые N записи. Для этого мы собираемся использовать те же методы, которые мы использовали в Last и нашем новом типе счетчика:

Давайте проверим это:

Что случилось?

Тип Drop будет повторяться до тех пор, пока Length<I> не совпадет с переданным нами значением N. Другими словами, тип индекса 0 выбирается условным средством доступа до тех пор, пока это условие не будет выполнено. И мы использовали Prepend<any, I>, чтобы можно было увеличить счетчик, как в цикле. Таким образом, Length<I> используется как счетчик рекурсии, и это способ свободно выполнять итерацию с TS.

Карри V3

Это был долгий и трудный путь, молодец! Для тебя есть награда 🥇.

Теперь предположим, что мы отслеживали, что наш карри потреблял 2 параметра:

Поскольку мы знаем количество потребляемых параметров, мы можем угадать те, которые еще остались для использования. Благодаря помощи Drop мы можем это сделать:

Похоже, Length и Drop - драгоценные инструменты. Итак, давайте обновим нашу предыдущую версию карри, в которой был неработающий Tail:

Что мы здесь делали?

Во-первых, Drop<Length<T>, P> означает, что мы удаляем использованные параметры.
Затем, если длина Drop<Length<T>, P> не равна 0, наш тип карри должен продолжать рекурсию с удаленными параметрами до тех пор, пока ... Наконец, когда все параметры были использованы, Length отброшенных параметров равно 0, а тип возврата - R.

Карри V4

Но у нас выше другая ошибка: TS жалуется, что наш Drop не относится к типу any[]. Иногда TS жалуется, что тип не тот, который вы ожидали, но вы знаете, что это так! Итак, давайте добавим в коллекцию еще один инструмент:

Бросать

Он требует, чтобы TS перепроверил тип X по типу Y, а тип Y будет принудительно применен только в случае сбоя. Таким образом, мы сможем остановить жалобы TS:

Давайте проверим это:

А это наш предыдущий карри, но на этот раз без претензий:

Помните, как раньше мы потеряли проверки типов, потому что начали отслеживать потребляемые параметры с T extends any[]? Ну, это было исправлено преобразованием T в Partial<P>. Мы добавили ограничение withCast<T,Partial<P>>!

Давайте проверим это:

Карри V5

Возможно, вы думали, что мы смогли снять параметры покоя. Что ж, мне очень жаль сообщить вам, что мы еще не там. Вот почему:

Поскольку остальные параметры могут быть неограниченными, TS предполагает, что длина нашего кортежа равна number, это довольно умно! Таким образом, мы не можем использовать Length при работе с остальными параметрами. Не грусти, все не так уж плохо:

Когда все параметры, не являющиеся остальными, используются, Drop<Length<T>,P> может соответствовать только […any[]]. Благодаря этому мы использовали [any,…any[] как условие для завершения рекурсии.

Давайте проверим это:

Все работает как шарм 🌹. Вы только что приобрели умный, общий, вариативный тип карри. Очень скоро ты сможешь поиграть с ним ... Но прежде, чем ты это сделаешь, что, если бы я сказал тебе, что наш тип может стать еще круче?

Заполнители

Как здорово? Мы собираемся дать нашему типу возможность понимать частичное применение любой комбинации аргументов в любой позиции. Согласно документации Ramda, это можно сделать с помощью заполнителя под названием _. В нем говорится, что для любой каррированной функции f эти вызовы эквивалентны:

Заполнитель или «пробел» - это объект, который абстрагирует тот факт, что мы
не способны или не хотим предоставить аргумент в определенный момент. Начнем с
определения, что такое заполнитель. Мы можем напрямую взять один из Ramda:

Ранее мы узнали, как выполнять наши первые итерации типа, увеличивая длину кортежа. На самом деле, использование Length и Prepend в нашем типе счетчика немного сбивает с толку. И чтобы было понятнее, с этого момента мы будем называть этот счетчик итератором. Вот несколько новых псевдонимов специально для этой цели:

Pos (положение)

Используйте его для запроса позиции итератора:

Далее (+1)

Он поднимает позицию итератора:

Назад (-1)

Это снижает позицию итератора:

Давайте проверим их:

Итератор

Он создает итератор (наш тип счетчика) в позиции, определенной Index, и может начать с позиции другого итератора, используя From:

Давайте проверим это:

Основные инструменты # 2

Хорошо, так что нам делать дальше? Нам нужно анализировать всякий раз, когда в качестве аргумента передается заполнитель. Оттуда мы сможем определить, был ли параметр «пропущен» или «отложен». Вот еще несколько инструментов для этой цели:

Задний ход

Вы не поверите, но нам все еще не хватает нескольких основных инструментов. Reverse даст нам свободу, в которой мы нуждаемся. Он берет кортеж T и превращает его наоборот в кортеж R благодаря нашим новым типам итераций:

Давайте проверим это:

Concat

И из Reverse родился Concat. Он просто берет кортеж T1 и объединяет его с другим кортежем T2. Примерно то же, что мы делали в test59:

Давайте проверим это:

Добавить

Включенный Concat, Append может добавлять тип E в конец кортежа T:

Давайте проверим это:

Анализ разрыва

Теперь у нас достаточно инструментов для выполнения проверки сложных типов. Но мы уже давно не обсуждали этот «пробел», как он снова работает? Если в качестве аргумента указан пробел, соответствующий ему параметр переносится на следующий шаг (который должен быть выполнен). Итак, давайте определим типы, которые понимают пробелы:

GapOf

Он проверяет наличие заполнителя в кортеже T1 в позиции, описанной итератором I. Если он найден, соответствующий тип собирается в той же позиции в T2 и переносится (сохраняется) для следующего шага через TN:

Давайте проверим это:

Пробелы

Пусть это меня не впечатлит. Он вызывает Gap через T1 & T2 и сохраняет результаты в TN. И когда это будет сделано, он объединяет результаты из TN с типами параметров, которые нужно принять (для следующего вызова функции):

Давайте проверим это:

Пробелы

Этот последний кусок головоломки нужно применить к отслеживаемым параметрам T. Мы будем использовать сопоставленные типы, чтобы объяснить, что можно заменить любой аргумент на заполнитель:

Сопоставленный тип позволяет выполнять итерацию и изменять свойства другого типа. В этом случае мы изменили T, чтобы каждая запись могла иметь тип заполнителя. И благодаря ? мы объяснили, что каждая запись T необязательна. Это означает, что нам больше не нужно использовать Partial для отслеживаемых параметров.

Давайте проверим это:

Ух, мы никогда не говорили, что можем взять undefined! Мы просто хотели опустить часть T. Это побочный эффект использования оператора ?. Но это не так уж и плохо, мы можем исправить это, переназначив NonNullable:

Итак, давайте соединим их вместе и получим то, что хотели:

Давайте проверим это:

Карри V6

Мы создали последние инструменты, которые нам когда-либо понадобятся для нашего типа карри. Пришло время собрать последние кусочки воедино. Напоминаю, что Gaps - это наша новая замена для Partial, а GapsOf заменит наш предыдущий Drop:

Давайте проверим это:

Чтобы убедиться, что все работает так, как задумано, я собираюсь принудительно установить значения, которые должны быть приняты функцией curried из примера:

Есть небольшая проблема: похоже, мы немного опередили Рамду! Наш тип может понимать очень сложные случаи использования заполнителей. Другими словами, заполнители Ramda просто не работают в сочетании с параметрами rest 😱:

Однако, даже если это выглядит совершенно правильно, это приведет к полному сбою. Это происходит потому, что реализация карри Ramda плохо справляется с комбинациями заполнителей и параметров отдыха. Вот почему я открыл тикет с Ramda на Github в надежде, что типы, которые мы только что создали, однажды смогут работать в гармонии с библиотекой.

Карри

Это очень мило, но нам нужно решить еще одну проблему: подсказки по параметрам. Не знаю, как вы, но я часто использую подсказки по параметрам. Очень полезно знать названия параметров, с которыми вы имеете дело. Версия выше не допускает подобных намеков. Вот исправление:

Признаюсь, это ужасно! Однако у нас есть подсказки для кода Visual Studio.
Что мы здесь сделали? Мы просто заменили типы параметров P & R, которые раньше обозначали типы параметров и тип возвращаемого значения соответственно. Вместо этого мы использовали тип функции F, из которого мы извлекли эквивалент P с Parameters<F> и R с ReturnType<F>. Таким образом, TypeScript может сохранять имя параметров даже после каррирования:

Есть только одна вещь: при использовании пробелов мы потеряем имя параметра.

Слово только для пользователей IntelliJ: правильные подсказки не помогут. Я рекомендую вам как можно скорее перейти на Visual Studio Code. И это управляемо сообществом, бесплатно, намного (намного) быстрее и поддерживает привязки клавиш для пользователей IntelliJ. :)

ПОСЛЕДНИЕ СЛОВА

Хочу сообщить вам, что вы только что прочитали руководство по ts-toolbelt. Это набор типов, который обеспечивает более высокую безопасность типов в TypeScript. Все началось с этой статьи, и вот несколько вещей, которые она может сделать для вас:

Вот и все. Я знаю, что сразу многое нужно переварить, поэтому я выпустил версию для разработчиков этой статьи. Вы можете клонировать его, протестировать и изменить с помощью TypeScript 3.3.x и выше. Держите его под рукой и извлекайте уроки, пока не освоитесь с различными техниками 📖.

Дайте пять 👏, если вам понравилось это руководство, и следите за моей следующей статьей!

РЕДАКТИРОВАТЬ: Это доступно для Ramda 0.26.1

Спасибо за чтение. И если у вас есть какие-либо вопросы или замечания,
пожалуйста, оставьте комментарий.