Начало работы с универсальным программированием на Scala и передовые практики универсального программирования

В этой статье исследуется, что такое универсальное программирование, как его можно использовать для решения проблемы повторного использования одного и того же кода Spark при изменении конкретных типов в конвейере и почему Shapeless в значительной степени является предпочтительным инструментом для универсального программирования на Scala. Я также представляю несколько советов по стилю работы с Shapeless.

Стоит отметить, что если вы действительно хотите пойти по этому пути в коде Spark, проект Frameless призван облегчить это.

TL; DR: Да, Shapeless того стоит. Нет альтернативы лучше. Он обеспечивает безопасность типов там, где другие методы не могут этого обеспечить, но требует значительных затрат времени, чтобы научиться писать. Его легко написать так, чтобы его было легко читать даже людям, которые этого не знают.

Что такое универсальное программирование и как в него вписывается Shapeless?

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

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

Фактически, наш пример будет сосредоточен на «целых числах», где «целые числа» на самом деле являются классами case.

Shapeless является ведущей библиотекой Scala для универсального программирования. Его большим преимуществом является то, что он не теряет безопасность типов и, по сути, предоставляет мини-язык на основе типов для программирования компилятора. Короче говоря, это предварительно записанная, проверенная компилятором опция в этом пространстве в Scala.

Зачем мне заниматься общим программированием?

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

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

Покажите мне, что проблема решается с помощью общего программирования прямо сейчас

Рассмотрим следующую проблему: мы хотим собрать воедино данные о том, когда определенные телеприставки настроены на определенные каналы (количество просмотров), объединить их с информацией о том, что воспроизводилось на этих каналах (расписания), и объединить их во временные сегменты (для простота запросов) в зависимости от количества просмотров и расписания. Позже информация о расписании изменяется, и поэтому нам нужно вычислить обновленные окончательные данные. Игнорируя возможность хранения наших промежуточных продуктов (размер данных и проблемы с управлением основными данными делают это относительно непривлекательным, если не создана необходимая инфраструктура), следующий вариант - повторно сопоставить данные расписания с окончательными данными.

Одно из решений - использовать решение с динамической типизацией (обычно Python или DataFrames на любом языке, поддерживаемом Spark). Эта опция позволяет нам писать незагроможденный код и во многих случаях является отличным решением. Действительно, возможность писать общий код без особых церемоний - огромная причина использовать динамические языки. Недостатком этого стиля программирования является отсутствие автоматической проверки во время компиляции того, что передаваемые данные соответствуют схеме, которую предполагает код. При работе с большим объемом данных это может значительно затруднить изменение кода, поскольку обычно неявные предположения о форме данных разбросаны по коду. (В качестве отступления: приветствуются любые общие предложения по решению этой проблемы). Подходы, основанные на отражении, страдают тем же недостатком проверки типов, хотя их может быть легче проверить.

Одно из решений проблемы допущения схемы - сделать схему явной и пометить код соответствующими типами. С конкретными видами это выглядит так:

Пока очень хорошо - мы сделали схемы явными, поместили их в наш список параметров, и компилятор проверит их за нас.

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

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

Общее решение. Итак, мы хотим указать, что viewership имеет столбцы времени, которые мы ожидаем. Мы также по-прежнему хотим просмотреть все столбцы, которые были просмотрены, но теперь мы должны учитывать, что будут конфликты между именами столбцов в наших окончательных данных (они будут иметь program_id) и данными расписания.

Давайте сначала разберемся с ограничениями типа:

Пока все выглядит хорошо. Теперь давайте займемся заполнением столбцов для выбора в строке 39. Поскольку мы используем Spark, мы, вероятно, могли бы восстановить имена столбцов из объектов DataSet. Это неплохое решение, но мы помещаем информацию о нашей схеме в классы case, а не в DataSet. Хотя я не могу придумать конкретную проблему с этим, мы предлагаем какую-то проблему с переводом, так что давайте ее избегать.

Сделайте глубокий вдох: это будет большая перемена.

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

Кстати, здесь нам не хватает одной меры безопасности: мы не проверяем автоматически, соответствует ли тип результата выбранным столбцам из типов ввода. Для этого нам нужно переместить объединение столбцов в параметры типа и сравнить с полями в RESULT. С этим изменением этот код станет пуленепробиваемым.

Написание этого кода было сложнее, чем его чтение - для Shapeless нет хорошей пошаговой документации, и часто мне приходилось обращаться к коду, чтобы узнать, как все это сочетается друг с другом. Часто компилятор генерирует полезные сообщения об ошибках, но иногда сообщения об ошибках уводили меня в погоню за дикой гусиной. Действительно, эта публикация на StackOverflow сыграла важную роль в том, чтобы заставить этот код работать.

Тем не менее, я, вероятно, мог бы прочитать это книжное руководство по Shapeless и многое почерпнуть таким образом; действительно, я, вероятно, прочитаю это.

Стоило ли? Сделал бы я это снова?

В целом, я думаю, что сделаю это еще раз. Обучение работе с Shapeless окупится по сравнению с его использованием. Я действительно ненавижу, когда мои ETL умирают в производственной среде, и я ненавижу искать проблемы с формой данных.

Создатели библиотек создают несколько замечательных универсальных интерфейсов, которые не требуют шаблонного кода со стороны пользователя библиотеки. Список некоторых из них здесь. Чем больше шаблонов вы создаете в своем собственном коде, тем больше я бы рекомендовал взглянуть на Shapeless.

Поговорим о стиле

В Руководстве по бесформенному астронавту целая глава посвящена стилю, и я также полагался на приведенное здесь руководство.

Посмотрите на приведенный выше код еще раз, и, надеюсь, вы заметите несколько вещей:

  • Описательные имена неявных параметров, которые представляют интерес позже в коде
  • Типы, которые мы хотим указать явно, ДОЛЖНЫ находиться в списке параметров, отличном от типов, которые мы хотим вычислить компилятором.
  • Комментарии объясняют, что подразумевается, потому что это код
  • Неявные параметры должны быть расположены в логическом порядке, ведущем к тому, что мы хотим
  • К сожалению, поскольку неявное разрешение происходит в текстовом порядке, нам пришлось поместить экземпляры ToTraversable в конце
  • Вам нужно поместить всю эту неявную магию в точку входа в общий код - теги TypeTags помогают сделать типы, помеченные таким образом (т.е. T в TypeTag[T]), доступными в теле функции, но поскольку типы стираются компилятором в соответствии с Согласно правилам стирания Java, компилятор не может полностью вывести необходимые последствия для дальнейших вложенных вызовов.

Заключение

См. TL; DR в верхней части статьи, но вкратце, если вам нравится безопасность шрифтов и вам не нравится копипаста, это хороший вариант. Другие ваши варианты, вероятно, будут включать в себя зону небезопасности и тестирования вокруг этого.

Если вы узнали из этой статьи, нажмите кнопку «Хлопок». Хлопки помогают привлечь внимание к этой статье большего числа читателей в сети Medium.