Смешивание общих признаков в параметризованных классах без дублирования параметров типа

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

val m = Map("name" -> "foo") with MoreFilterOperations

и иметь методы для MoreFilterOperations, которые выражены во всем, что может предложить Traversable, например:

def filterFirstTwo(f: (T) => Boolean) = filter(f) take 2

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

val m = Map("name" -> "foo") with MoreFilterOperations[(String,String)]

или если я определю переменную этого типа:

var m2: Map[String,String] with MoreFilterOperations[(String,String)] = ...

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

var m2: Map[String,String] with MoreFilterOperations

Я пробовал собственные типы, члены абстрактных типов, но это не принесло ничего полезного. Есть какие-нибудь подсказки?

3 ответа

Решение

Map("name" -> "foo") является вызовом функции, а не конструктором, это означает, что вы не можете написать:

Map("name" -> "foo") with MoreFilterOperations

больше, что вы можете написать

val m = Map("name" -> "foo")
val m2 = m with MoreFilterOperations

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

def EnhMap[K,V](entries: (K,V)*) =
  new collection.immutable.HashMap[K,V] with MoreFilterOptions[(K,V)] ++ entries

Использование фабричного метода здесь, чтобы избежать дублирования параметров типа. Однако это не сработает, потому что ++ метод просто собирается вернуть старую HashMapбез миксина!

Решение (как предложил Сэм) состоит в том, чтобы использовать неявное преобразование для добавления метода pimped. Это позволит вам трансформировать карту всеми обычными методами, и при этом вы сможете использовать дополнительные методы на полученной карте. Обычно я делаю это с классом вместо черты, так как наличие доступных параметров конструктора приводит к более чистому синтаксису:

class MoreFilterOperations[T](t: Traversable[T]) {
  def filterFirstTwo(f: (T) => Boolean) = t filter f take 2
}

object MoreFilterOperations {
  implicit def traversableToFilterOps[T](t:Traversable[T]) =
    new MoreFilterOperations(t)
}

Это позволяет вам тогда написать

val m = Map("name"->"foo", "name2"->"foo2", "name3"->"foo3")
val m2 = m filterFirstTwo (_._1.startsWith("n"))

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

import collection.TraversableLike

class MoreFilterOperations[Repr <% TraversableLike[T,Repr], T] (xs: Repr) {
  def filterFirstTwo(f: (T) => Boolean) = xs filter f take 2
}

Достаточно просто. Вы должны поставить Reprтип, представляющий коллекцию, и Tтип элементов. я использую TraversableLike вместо Traversable как он встраивает свое представление; без этого filterFirstTwo вернет Traversable независимо от типа запуска.

Теперь неявные преобразования. Это где вещи становятся немного хитрее в нотации типов. Во-первых, я использую тип с более высоким родом, чтобы захватить представление коллекции: CC[X] <: Traversable[X]этот параметр параметризует CC тип, который должен быть подклассом Traversable (обратите внимание на использование X в качестве заполнителя здесь, CC[_] <: Traversable[_] не значит одно и то же).

Там также неявное CC[T] <:< TraversableLike[T,CC[T]], который компилятор использует для статической гарантии того, что наша коллекция CC[T] действительно подкласс TraversableLike и поэтому действительный аргумент для MoreFilterOperations конструктор:

object MoreFilterOperations {
  implicit def traversableToFilterOps[CC[X] <: Traversable[X], T]
  (xs: CC[T])(implicit witness: CC[T] <:< TraversableLike[T,CC[T]]) =
    new MoreFilterOperations[CC[T], T](xs)
}

Все идет нормально. Но есть еще одна проблема... Это не будет работать с картами, потому что они принимают два параметра типа. Решение состоит в том, чтобы добавить еще одно неявное MoreFilterOperations объект, используя те же принципы, что и раньше:

implicit def mapToFilterOps[CC[KX,VX] <: Map[KX,VX], K, V]
(xs: CC[K,V])(implicit witness: CC[K,V] <:< TraversableLike[(K,V),CC[K,V]]) =
  new MoreFilterOperations[CC[K,V],(K,V)](xs)

Настоящая красота проявляется, когда вы также хотите работать с типами, которые на самом деле не являются коллекциями, но могут рассматриваться, как если бы они были. Помните Repr <% TraversableLike в MoreFilterOperations конструктор? Это ограничение вида и позволяет типы, которые могут быть неявно преобразованы в TraversableLike а также прямые подклассы. Строки являются классическим примером этого:

implicit def stringToFilterOps
(xs: String)(implicit witness: String <%< TraversableLike[Char,String])
: MoreFilterOperations[String, Char] =
  new MoreFilterOperations[String, Char](xs)

Если вы сейчас запустите его на REPL:

val m = Map("name"->"foo", "name2"->"foo2", "name3"->"foo3")
//  m: scala.collection.immutable.Map[java.lang.String,java.lang.String] =
//    Map((name,foo), (name2,foo2), (name3,foo3))

val m2 = m filterFirstTwo (_._1.startsWith("n"))
//  m2: scala.collection.immutable.Map[java.lang.String,java.lang.String] =
//    Map((name,foo), (name2,foo2))

"qaxfwcyebovjnbointofm" filterFirstTwo (_ < 'g')
//res5: String = af

Карта входит, Карта выходит. Строка входит, строка выходит. так далее...

Я не пробовал это с Stream еще или Setили Vector, но вы можете быть уверены, что если вы это сделаете, он вернет коллекцию того же типа, с которой вы начали.

Это не совсем то, что вы просили, но вы можете решить эту проблему с последствиями:

trait MoreFilterOperations[T] {
  def filterFirstTwo(f: (T) => Boolean) = traversable.filter(f) take 2
  def traversable:Traversable[T]
}

object FilterImplicits {
  implicit def traversableToFilterOps[T](t:Traversable[T]) = new MoreFilterOperations[T] { val traversable = t }
}

object test {

  import FilterImplicits._

  val m = Map("name" -> "foo", "name2" -> "foo2", "name3" -> "foo3")
  val r = m.filterFirstTwo(_._1.startsWith("n"))
}

scala> test.r
res2: Traversable[(java.lang.String, java.lang.String)] = Map((name,foo), (name2,foo2))

Стандартная библиотека Scala использует для этой цели последствия. Например "123".toInt, Я думаю, что это лучший способ в этом случае.

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

С изменяемыми коллекциями вы можете сделать что-то вроде этого:

object FooBar {
  trait MoreFilterOperations[T] {
    this: Traversable[T] =>
    def filterFirstTwo(f: (T) => Boolean) = filter(f) take 2
  }

  object moreFilterOperations {
    def ~:[K, V](m: Map[K, V]) = new collection.mutable.HashMap[K, V] with MoreFilterOperations[(K, V)] {
      this ++= m
    }
  }

  def main(args: Array[String]) {
    val m = Map("a" -> 1, "b" -> 2, "c" -> 3) ~: moreFilterOperations
    println(m.filterFirstTwo(_ => true))
  }
}

Я бы предпочел использовать последствия.

Другие вопросы по тегам