- PVSM.RU - https://www.pvsm.ru -

Type classes в Scala

Type classes в Scala
В последнее время в сообществе Scala-разработчиков стали уделять всё большее внимание шаблону проектирования Type classes. Он помогает бороться с лишними зависимостями и в то же время делать код чище. Ниже на примерах я покажу, как его применять и какие у такого подхода есть преимущества.

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

Предпосылки

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

Для решения этих задач в Java-мире существует несколько методов:

  • написать интерфейс и имплементировать его в самом классе (например, Comparable),
  • написать отдельный интерфейс, специфичный экземпляр которого будет получать объект класса и проводить необходимые действия (например, Comparator);
  • для отдельных задач даже есть специальные методы:
    • для создания объектов — шаблон Factory,
    • для связки со сторонними библиотеками используется Adapter,
    • для сериализации и чтения иногда используют Reflection. Это напоминает вскрытие живота из недавнего поста [1], но операции на животе тоже иногда нужны, сразу оговорюсь, Type classes не смогут его заменить.

Итак, по сути мы имеем сейчас две альтернативы: или жёстко связывать сторонний функционал с классом или выносить функционал в отдельный объект и потом заботиться о его передаче. В качестве примера возьмём пользователей приложения. Так может выглядеть добавление возможности сравнивать наших пользователей:

// Определение метода, которому нужно сравнение
def sort[T <: Comparable[T]](elements: Seq[T]): Seq[T]

// Определение класса
class Person(val id: Id[Person], val name: String) extends Comparable[Person] {

  def compareTo(Person anotherPerson): Int = {
    return this.name.compareTo(anotherPerson.name)
  }
}

// Использование
sort(persons)

Здесь хорошо, что конкретное применение сортировки выглядит очень ясно. Но логика сравнения жёстко связана с классом (а наверно никто не любит, что объекты модели зависят, например, от библиотеки формирующей XML и пишущей в БД). Кроме того появляется ещё одна проблема: нельзя определить больше одного способа сравнения — то есть если завтра мы захотим сравнивать пользователей по id в другом месте программы, ничего у нас не получится, переписать метод сравнения для закрытых классов также не удастся.

В Java для этой цели есть класс Comparator, он позволяет получить большую гибкость:

// Определение метода, которому нужно сравнение
def sort[T](elements: Seq[T], comparator: Comparator[T]): Seq[T]

// Определение класса
class Person(val id: Id[Person], val name: String)

trait Comparator[T] {
  def compare(T object1, T object2): Int
}

class PersonNameComparator extends Comparator[Person] {
  def compare(Person onePerson, Person anotherPerson): Int = {
    return onePerson.name.compareTo(anotherPerson.name)
  }
}

// Использование
val nameComparator = new PersonNameComparator()
sort(persons, nameComparator)

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

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

И тут появляются Type classes

Вот тут на помощь приходит возможность Scala неявно (implicit) передавать параметры. Давайте возьмём за основу наш предыдущий пример, но определим алгоритм иначе, будем передавать Comparator неявно:

def sort[T](elements: Seq[T])(implicit comparator: Comparator[T]): Seq[T]

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

implicit val personNameComparator = Comparator[Person] {
  def compare(Person onePerson, Person anotherPerson): Int = {
    return onePerson.name.compareTo(anotherPerson.name)
  }
}

Ключевое слово implicit отвечает за то, что значение будет использоваться при подстановке. Важно отметить, что наши неявные реализации должны быть stateless, поскольку в процессе работы программы создаётся всего один экземпляр каждого типа.

Теперь сортировку можно вызывать также, как это было в изначальном варианте с реализацией Comparable:

// Вызываем сортировку
sort(persons)

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

Чуть более интересный вариант возникает, когда хочется, чтобы параметр типа мог быть сам типизирован. То есть для Map[Id[Person],List[Permission]] мы хотим MapJsonSerializer, IdJsonSerializer, ListJsonSerializer и PermissionJsonSerializer, которые можно переиспользовать в любом порядке, а не PersonPermissionsMapJsonSerializer, аналоги которого мы будем писать каждый раз. В таком случае способ определения неявного объекта немного отличается, теперь у нас не объект, а функция:

implicit def ListComparator[V](implicit comparator: Comparator[V]) = new Comparator[List[V]] {
  def compare(oneList: List[V], anotherList: List[V]): Int = {
    for((one, another) <- oneList.zip(anotherList)) {
      val elementsCompared = comparator,compare(one, another)
      if(elementsCompared > 0) return 1
      else if(elementsCompared < 0) return -1
    }

    return 0
  }
}

Вот собственно и весь метод. Самая прелесть в том, что так можно получать всякие JSONParser’ы, XMLSerializer’ы вместе с PersonFactory, нигде не храня соответствия классов и объектов — компиллятор Scala всё сделает за нас.

В ТКС мы используем такой метод, например, чтобы оборачивать исключения в классы нашей модели. Type classes позволяют создавать экземлпяры исключений того типа, в который надо обернуть брошенное блоком. Если бы это делалось традиционным методом, пришлось бы создать и передавать фабрику исключений, так что проще было по старинке кидать исключения руками. Теперь всё красиво.

Что дальше?

На самом деле, тема Type classes тут не заканчивается и в качестве продолжения рекомендую видео Typeclasses in Scala [2].

Ещё более фундаментально вопрос изложен в этой статье [3].

Автор: vuspenskiy


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/scala/11385

Ссылки в тексте:

[1] недавнего поста: http://habrahabr.ru/post/147148/

[2] Typeclasses in Scala: http://www.youtube.com/watch?v=sVMES4RZF-8

[3] этой статье: http://ropas.snu.ac.kr/~bruno/papers/TypeClasses.pdf