При написании Firebase REST client for Scala мне приходилось думать о том, как лучше всего перевести семантику REST в типы и вызовы функций, и одним из самых интересных вопросов был: какой тип возвращаемого значения должен иметь метод, какой представляет собой GET для определенного ресурса.

Соглашения REST довольно ясны: GET /users/{id} даст вам либо 404, либо имя пользователя. Также иногда можно выполнять поисковые запросы: GET /users?id=someone%40example.com. Что обычно означает: либо пустой массив, либо один объект.

Давайте представим, мы пишем клиент БД, который умеет то же самое: возвращать пользователя по id. Как и выше, есть две возможности: найдено и не найдено. И метод может быть одним из следующих:

  • def get[T](id: String): Future[Option[T]]выполняется в будущем независимо от существования ресурса, содержание отличается;
  • def get[T](id: String): Future[T] будущее выполняется успешно, если ресурс существует, и терпит неудачу, если его нет. Конечно, может быть на 100% законно, что ресурса не существует. Пользователю этого метода придется иметь дело с этим, например, с помощью recoverWith().

Кажется, есть и третий вариант: объединить лучшее из двух миров. Параметр типа T позволяет нам не только параметризовать тип возвращаемого значения, но фактически также определить семантику функции:

  • get[Option[User]](“[email protected]”) будет использовать параметр типа возвращаемого типа, чтобы указать, пользователь существует. Успех/Неудача Будущего будет зарезервирован для любых других ошибок. Это полезно, если вы хотите обрабатывать случай «не найдено» иначе, чем, например, ошибку соединения.
  • get[User](“[email protected]”) будет иметь другую семантику. Вы запрашиваете пользователя, вы получите либо его, либо Failure. Это упрощает задачу, когда вы знаете, что пользователь должен существовать, а его отсутствие сродни непонятной StackOverflowError в драйвере базы данных.

Как мы это реализуем? Определение того, как это должно работать, является хорошим началом:

Одна из возможных реализаций с использованием тегов класса и отражения: мы проверяем параметр типа, чтобы увидеть, является ли он Option, и на основании этого по-разному обрабатываем извлечение данных (data.get( id) vs data(id)).. Не пугайтесь instanceOf, у вас, вероятно, будет что-то умнее при использовании настоящей библиотеки для запросов ОТДЫХ.

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

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

UPD1: использование отражения (например, ClassTag) может привести к серьезному снижению производительности. Эта реализация представлена ​​в качестве иллюстрации из-за ее простоты. Вероятно, вы захотите использовать классы типов при создании API производственного качества.