Как правило, люди думают, что объектно-ориентированное программирование (ООП) и функциональное программирование (ФП) взаимно исключают друг друга. И это объяснимо: любая дискуссия о них довольно часто превращается в спор, в основе которого лежит соперничество. Но, как оказалось, в некоторых контекстах можно извлечь выгоду, применяя дисциплины из обеих парадигм, и взять таким образом лучшее из двух миров. Один из таких контекстов — ad-hoc полиморфизм в Typescript.
Сценарий
Рассмотрим ситуацию, когда мы разрабатываем встроенную библиотеку стилей. Библиотека содержит различные типы, соответствующие разным типам данных CSS, таким как:
- RGB.
- RGBA.
- HSL.
- HSLA.
- HEX.
- Length.
- Percentage.
- Decibel.
- Time.
- Transformation.
Для простоты наш интерфейс для этих объектов будет поддерживать только одно свойство — color
.
Как показано на картинке, значение свойства color
в нашем интерфейсе имеет тип tagged-union
, который состоит из типа stringLiteral
и пяти других пользовательских типов, представленных в виде объектов Javascript и смоделированных по модели nominal-typing.
В пределах кода библиотеки, эти типы хорошо понятны, и их значениями можно соответственно управлять, но вне этих границ — скажем в React, — они уже лишаются смысла. Чтобы React мог пользоваться этими значениями для определения стилей, нам нужно сериализовать их в строковые (string
) или числовые (number
) значения.
Задача
Исходя из сценария, нам нужна функция, которая сериализует значение цвета в строковое значение. Можно сделать это следующим способом:
Ничего необычного здесь нет. Мы создаем небольшие функции, каждая из которых отвечает за сериализацию одного подтипа (color
), а затем создаем функцию, которая проверяет тип передаваемого аргумента, используя защиту типов, и вызывает функцию, связанную с этим типом. Такой подход приводит к избытку кода и увеличению числа операций импорта (в данном случае их пять).
В Haskell для того, чтобы разобраться с этим сценарием, можно было бы воспользоваться ad-hoc полиморфизмом. В таком случае у вас будут разные реализации под одним и тем же именем, связанные с разными типами, и Haskell автоматически выберет подходящую реализацию в зависимости от типа аргумента. Если бы в Typescript было нечто подобное, оно бы выглядело примерно так (за минусом, разумеется, сообщения об ошибке):
Как видите, компилятор TS жалуется на “дублирующую реализацию функции”. Эта ошибка возникает из-за того, что у вас может быть несколько подписей под одним и тем же именем, но разрешена только одна реализация. Эта реализация отвечает за поиск соответствующей функции для ее вызова. Поэтому, чтобы исправить ошибку, нужно сделать следующее:
И в этом нет никакого смысла! Мы оказались в той же ситуации, что и раньше, только хуже, потому что кода стало больше. Typescript называет это перегрузкой функций. Она может быть удобна во многих случаях, но не в этом.
Перегрузка функций в TS сработала не так, как ожидалось. И тут вы унываете, голос в голове призывает вас обратиться к темной стороне, где бедные объекты кричат от боли, вызванной мутациями и привязкой метода. Но затем, как раз перед тем, как вы сделаете свой первый шаг во вселенную ООП, появляюсь я, ваш ангел-хранитель, и останавливаю вас, предлагая золотую середину — безопасное место, где ваши функции вместе с объектами могут процветать в чистоте и довольстве.
Решение
Мой подход к этой проблеме заключается в том, чтобы сохранить ссылку на функцию внутри типизированного объекта, выкраденного прямиком из вселенной ООП. В этом подходе нет никаких состояний, и функция все еще прозрачна. Упомянутая ссылка предназначена только для того, чтобы обеспечить легкий и быстрый доступ к соответствующей функции и устранить избыточность кода. Мы можем сделать это, добавив свойство, значение которого будет ссылкой на функцию. Что-то вроде такого:
Свойства type
и data
можно игнорировать. Функция rgb
— это просто фабричная функция, которая создает значения типа RGB
. Результирующий объект будет иметь свойство serialize
, указывающее на функцию serializeRGB
. Следуя этому шаблону с другими нашими пользовательскими типами, мы можем реорганизовать функцию serializeColor
в следующий вид:
Каждый тип указывает на собственную реализацию поведения сериализации. Никакой необходимости в импорте или ручной проверке типов.
Надеюсь, вы согласитесь, что это намного аккуратнее предыдущей версии. Но если и нет, то все в порядке. Мы все имеем право на свое мнение.
Выводы
- Можно внести некоторые моменты из ООП в функциональный код, не нарушая его чистоту.
- Ссылки — отличный способ реализации полиморфизма типа ad-hoc в ТypeScript.
Сценарий, изложенный в этой статье, на самом деле представляет собой упрощенную версию того, с которым я столкнулся в Rosebox, библиотеке стилей, которую я написал в Typescript. Можете проверить.
Читайте также:
- Ох, TypeScript, ты боль моя
- React TypeScript: Основы и лучшие практики
- Веб-сервер с нуля в TypeScript и Node
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи: Hugo Nteifeh, “Polymorphism in Typescript”