Смешивание общих признаков в параметризованных классах без дублирования параметров типа
Давайте предположим, что я хочу создать черту, которую я могу смешать с любым 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))
}
}
Я бы предпочел использовать последствия.