Для чего нужны классы типов в Scala? - PullRequest
66 голосов
/ 23 марта 2011

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

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

Я нашел это хорошим, но не особенно полезным. Не могли бы вы привести пример использования / пример, который показывает, для чего эта функция полезна?

Ответы [ 11 ]

84 голосов
/ 25 марта 2011

Один вариант использования, согласно запросу ...

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

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

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

Тогда для исходной задачи вы могли бы реализовать класс типа Addable:

trait Addable[T] {
  def zero: T
  def append(a: T, b: T): T
}
//yup, it's our friend the monoid, with a different name!

Затем вы можете создать неявные подклассы этого экземпляра, соответствующие каждому типу, который вы хотите сделать добавляемым:

implicit object IntIsAddable extends Addable[Int] {
  def zero = 0
  def append(a: Int, b: Int) = a + b
}

implicit object StringIsAddable extends Addable[String] {
  def zero = ""
  def append(a: String, b: String) = a + b
}

//etc...

Метод суммирования списка становится тривиальным для записи ...

def sum[T](xs: List[T])(implicit addable: Addable[T]) =
  xs.FoldLeft(addable.zero)(addable.append)

//or the same thing, using context bounds:

def sum[T : Addable](xs: List[T]) = {
  val addable = implicitly[Addable[T]]
  xs.FoldLeft(addable.zero)(addable.append)
}

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

Кстати, это именно тот подход, который используется в API 2.8 коллекций. Хотя метод sum определен для TraversableLike, а не для List, а классом типа является Numeric (он также содержит несколько больше операций, чем просто zero и append)

32 голосов
/ 23 марта 2011

Перечитайте первый комментарий там:

Важное различие между классами типов и интерфейсами состоит в том, что для того, чтобы класс А был «членом» интерфейса, он должен объявить об этом на своем собственном определении. Напротив, любой тип может быть добавлен в класс типов в любое время, при условии, что вы можете предоставить требуемые определения, и поэтому члены класса типов в любой момент времени зависят от текущей области. Поэтому нас не волнует, ожидал ли создатель A класса типов, к которому мы хотим, чтобы он принадлежал; в противном случае мы можем просто создать наше собственное определение, показывающее, что оно действительно принадлежит, и затем использовать его соответствующим образом. Таким образом, это не только обеспечивает лучшее решение, чем адаптеры, в некотором смысле это устраняет целую проблему, для решения которой предназначались адаптеры.

Я думаю, что это самое важное преимущество классов типов.

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

case class Default[T](val default: T)

object Default {
  implicit def IntDefault: Default[Int] = Default(0)

  implicit def OptionDefault[T]: Default[Option[T]] = Default(None)

  ...
}
9 голосов
/ 24 марта 2011

Я считаю классы типов способностью добавлять метаданные, безопасные для типов, в класс.

Итак, вы сначала определяете класс для моделирования проблемной области, а затем думаете о метаданных, которые нужно добавить в нее.Такие вещи, как Equals, Hashable, Viewable и т. Д. Это создает разделение проблемной области и механизмов для использования класса и открывает подклассы, потому что класс меньше.

Кроме этого, вы можете добавлять классы типов в любом месте области видимости, а не только там, где класс определен, и вы можете изменять реализации.Например, если я вычисляю хеш-код для класса Point с помощью Point # hashCode, я ограничиваюсь этой конкретной реализацией, которая может не создать хорошего распределения значений для определенного набора точек, которые у меня есть.Но если я использую Hashable [Point], то я могу предоставить свою собственную реализацию.

[Обновлено с примером] В качестве примера, вот пример использования, который у меня был на прошлой неделе.В нашем продукте есть несколько случаев, когда Карты содержат контейнеры в качестве значений.Например, Map[Int, List[String]] или Map[String, Set[Int]].Добавление к этим коллекциям может быть многословным:

map += key -> (value :: map.getOrElse(key, List()))

Поэтому я хотел иметь функцию, которая оборачивает это, чтобы я мог написать

map +++= key -> value

Основная проблема заключается в том, что коллекции неу всех одинаковые методы добавления элементов.У некоторых есть «+», а у других «+».Я также хотел сохранить эффективность добавления элементов в список, поэтому я не хотел использовать fold / map для создания новых коллекций.

Решение заключается в использовании классов типов:

  trait Addable[C, CC] {
    def add(c: C, cc: CC) : CC
    def empty: CC
  }

  object Addable {
    implicit def listAddable[A] = new Addable[A, List[A]] {
      def empty = Nil

      def add(c: A, cc: List[A]) = c :: cc
    }

    implicit def addableAddable[A, Add](implicit cbf: CanBuildFrom[Add, A, Add]) = new Addable[A, Add] {
      def empty = cbf().result

      def add(c: A, cc: Add) = (cbf(cc) += c).result
    }
  }

Здесь я определил класс типов Addable, который может добавить элемент C в коллекцию CC.У меня есть 2 реализации по умолчанию: для списков, использующих ::, и для других коллекций, используя каркас построителя.

Затем с помощью этого класса типов:

class RichCollectionMap[A, C, B[_], M[X, Y] <: collection.Map[X, Y]](map: M[A, B[C]])(implicit adder: Addable[C, B[C]]) {
    def updateSeq[That](a: A, c: C)(implicit cbf: CanBuildFrom[M[A, B[C]], (A, B[C]), That]): That  = {
      val pair = (a -> adder.add(c, map.getOrElse(a, adder.empty) ))
      (map + pair).asInstanceOf[That]
    }

    def +++[That](t: (A, C))(implicit cbf: CanBuildFrom[M[A, B[C]], (A, B[C]), That]): That  = updateSeq(t._1, t._2)(cbf)
  }

  implicit def toRichCollectionMap[A, C, B[_], M[X, Y] <: col

Специальный бит использует adder.add для добавления элементов и adder.empty для создания новых коллекций для новых ключей.

Для сравнения, без классов типов у меня было бы 3 варианта: 1. написать метод для каждого типа коллекции.Например, addElementToSubList и addElementToSet и т. Д. Это создает много шаблонного элемента в реализации и загрязняет пространство имен 2. Чтобы использовать отражение, чтобы определить, является ли подколлекция списком / набором.Это сложно, так как карта пуста для начала (конечно, scala помогает и с манифестами) 3. иметь класс типа бедняков, требуя от пользователя предоставить сумматор.Так что-то вроде addToMap(map, key, value, adder), что просто уродливо

6 голосов
/ 23 марта 2011

Еще один способ, которым я нахожу этот пост в блоге полезным, это то, где он описывает классы типов: Монады не являются метафорами

Поиск в статье для класса типов. Это должен быть первый матч. В этой статье автор приводит пример класса типов Monad.

5 голосов
/ 03 июля 2016

В ветке форума " Что делает классы типов лучше, чем черты? ", некоторые интересные моменты:

  • Классы типов могут очень легко представлять понятия, которые довольно трудно представить при наличии подтипов, такие как равенство и упорядочение .
    Упражнение: создайте небольшую иерархию классов / признаков и попробуйте реализовать .equals для каждого класса / свойства таким образом, чтобы операция над произвольными экземплярами из иерархии была правильно рефлексивной, симметричной и транзитивной.
  • Классы типов позволяют вам предоставлять доказательства того, что тип, находящийся за пределами вашего "элемента управления", соответствует некоторому поведению.
    Чей-то тип может быть членом вашего класса типов.
  • Вы не можете выразить «этот метод принимает / возвращает значение того же типа, что и получатель метода» в терминах подтипирования, но это (очень полезное) ограничение является простым при использовании классов типов. Это проблема f-ограниченных типов (где F-ограниченный тип параметризован по своим собственным подтипам).
  • Для всех операций, определенных для черты, требуется экземпляр ; всегда есть аргумент this. Таким образом, вы не можете определить, например, метод fromString(s:String): Foo для trait Foo таким образом, что вы можете вызывать его без экземпляра Foo.
    В Scala это проявляется в том, что люди отчаянно пытаются абстрагироваться от сопутствующих объектов.
    Но с классом типов это просто, как показано нулевым элементом в этом моноидном примере .
  • Типы классов могут быть определены индуктивно ; например, если у вас есть JsonCodec[Woozle], вы можете получить JsonCodec[List[Woozle]] бесплатно.
    Приведенный выше пример иллюстрирует это для «вещей, которые вы можете сложить вместе».
4 голосов
/ 20 июля 2013

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

1 голос
/ 21 февраля 2017

В классах типа scala

  • Включает специальный полиморфизм
  • Статически типизированный (т.е. безопасный для типов)
  • Заимствовано из Haskell
  • Решает проблему выражения

Поведение может быть расширено - во время компиляции - после факта - без изменения / перекомпиляции существующего кода

Scala Implicits

Последний параметрсписок методов может быть помечен как неявный

  • Неявные параметры заполняются компилятором

  • В действительности вам требуется подтверждение компилятора

  • … например, наличие класса типа в области действия

  • При необходимости вы также можете указать параметры явно

Расширение нижеприведенного примера в классе String с реализацией класса типов расширяет класс новыми методами, даже если строка является окончательной :)

/**
* Created by nihat.hosgur on 2/19/17.
*/
case class PrintTwiceString(val original: String) {
   def printTwice = original + original
}

object TypeClassString extends App {
  implicit def stringToString(s: String) = PrintTwiceString(s)
  val name: String = "Nihat"
  name.printTwice
}
1 голос
/ 11 января 2017

Оба подразумевают и Классы типов используются для Преобразование типов .Основной вариант использования для них обоих - предоставить специальный полиморфизм (то есть) для классов, которые вы не можете изменить, но ожидаете наследственного вида полиморфизма.В случае имплицитов вы можете использовать как неявный def, так и неявный класс (который является вашим классом-оберткой, но скрыт от клиента).Классы типов являются более мощными, поскольку они могут добавить функциональность в уже существующую цепочку наследования (например: Упорядочение [T] в функции сортировки scala).Для более подробной информации вы можете увидеть https://lakshmirajagopalan.github.io/diving-into-scala-typeclasses/

1 голос
/ 31 июля 2014

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

0 голосов
/ 29 марта 2018

Мне нравится использовать классы типов в качестве упрощенной идиоматической формы Scala Dependency Injection, которая все еще работает с циклическими зависимостями, но не добавляет много сложности кода.Недавно я переписал проект Scala с использования Cake Pattern для ввода классов для DI и добился сокращения размера кода на 59%.

...