Scala: укажите типовой тип по умолчанию вместо Nothing
У меня есть пара классов, которые выглядят примерно так. Там a Generator
, который генерирует значение, основанное на некоторых значениях уровня класса, и a GeneratorFactory
, который строит a Generator
.
case class Generator[T, S](a: T, b: T, c: T) {
def generate(implicit bf: CanBuildFrom[S, T, S]): S =
bf() += (a, b, c) result
}
case class GeneratorFactory[T]() {
def build[S <% Seq[T]](seq: S) = Generator[T, S](seq(0), seq(1), seq(2))
}
Вы заметите, что GeneratorFactory.build
принимает аргумент типа S
и Generator.generate
создает значение типа S
, но нет ничего типа S
, сохраненного Generator
.
Мы можем использовать классы, подобные этому. factory работает с последовательностью Char
, а generate
создает a String
, потому что build
задается String
.
val gb = GeneratorFactory[Char]()
val g = gb.build("this string")
val o = g.generate
Это нормально и обрабатывает тип String
неявно, потому что мы используем GeneratorFactory
.
Проблема
Теперь проблема возникает, когда я хочу построить Generator
, не пройдя через factory. Я хотел бы иметь возможность сделать это:
val g2 = Generator('a', 'b', 'c')
g2.generate // error
Но я получаю сообщение об ошибке, потому что g2
имеет тип Generator[Char,Nothing]
и Scala "Невозможно построить коллекцию типа Nothing с элементами типа Char на основе коллекции типа Nothing."
То, что я хочу, это способ сказать Scala, что значение по умолчанию S
похоже на Seq[T]
вместо Nothing
. Заимствуя синтаксис для параметров по умолчанию, мы могли бы подумать об этом как о чем-то вроде:
case class Generator[T, S=Seq[T]]
Недостаточные решения
Конечно, это работает, если явным образом расскажу генератору о том, каким должен быть его сгенерированный тип, но я думаю, что вариант по умолчанию будет более приятным (мой фактический сценарий более сложный):
val g3 = Generator[Char, String]('a', 'b', 'c')
val o3 = g3.generate // works fine, o3 has type String
Я думал о перегрузке Generator.apply
, чтобы иметь версию с одним родовым типом, но это вызывает ошибку, поскольку, по-видимому, Scala не может различать два определения apply
:
object Generator {
def apply[T](a: T, b: T, c: T) = new Generator[T, Seq[T]](a, b, c)
}
val g2 = Generator('a', 'b', 'c') // error: ambiguous reference to overloaded definition
Желаемый выход
Я хотел бы просто создать Generator
без указания типа S
и по умолчанию использовать его Seq[T]
, чтобы я мог:
val g2 = Generator('a', 'b', 'c')
val o2 = g2.generate
// o2 is of type Seq[Char]
Я думаю, что это был бы самый чистый интерфейс для пользователя.
Любые идеи, как я могу это сделать?
Ответы
Ответ 1
Есть ли причина, по которой вы не хотите использовать базовый признак, а затем узко S
по мере необходимости в своих подклассах? Следующие, например, соответствуют вашим требованиям:
import scala.collection.generic.CanBuildFrom
trait Generator[T] {
type S
def a: T; def b: T; def c: T
def generate(implicit bf: CanBuildFrom[S, T, S]): S = bf() += (a, b, c) result
}
object Generator {
def apply[T](x: T, y: T, z: T) = new Generator[T] {
type S = Seq[T]
val (a, b, c) = (x, y, z)
}
}
case class GeneratorFactory[T]() {
def build[U <% Seq[T]](seq: U) = new Generator[T] {
type S = U
val Seq(a, b, c, _*) = seq: Seq[T]
}
}
Я создал S
абстрактный тип, чтобы он немного отличался от способа пользователя, но вы могли бы также сделать его параметром типа.
Ответ 2
Это напрямую не отвечает на ваш основной вопрос, так как, как я думаю, другие справляются с этим. Скорее, это ответ на ваш запрос значений по умолчанию для аргументов типа.
Я подумал над этим, даже до тех пор, пока я начинаю писать предложение об изменении языка, чтобы это разрешить. Однако я остановился, когда понял, откуда начинается Ничто. Это не какая-то "стандартная ценность", как я ожидал. Я попытаюсь объяснить, откуда оно взялось.
Чтобы назначить тип аргументу типа, Scala использует наиболее конкретный возможный/юридический тип. Так, например, предположим, что у вас есть "класс A [T] (x: T)", и вы говорите "новый A [Int]". Вы прямо указали значение "Int" для T. Теперь предположим, что вы говорите "новый A (4)". Scala знает, что 4 и T должны иметь один и тот же тип. 4 может иметь тип где-нибудь между "Int" и "Any". В этом типе диапазона "Int" является наиболее конкретным типом, поэтому Scala создает "A [Int]". Теперь предположим, что вы говорите "новый A [AnyVal]". Теперь вы ищете наиболее специфичный тип T такой, что Int <: T <: Any и AnyVal <: T <: AnyVal. К счастью, Int <: AnyVal <: Any, поэтому T может быть AnyVal.
Продолжая, теперь предположим, что у вас есть "класс B [S > : String <: AnyRef]". Если вы скажете "новый B", вы не получите B [Nothing]. Скорее вы обнаружите, что получаете B [String]. Это связано с тем, что S ограничено как String <: S <: AnyRef и String находятся в нижней части этого диапазона.
Итак, вы видите, что для "класса C [R]" "новый C" не дает вам C [Nothing], потому что Nothing является своего рода значением по умолчанию для аргументов типа. Скорее, вы получаете C [Nothing], потому что ничто не является наименьшим, что может быть R (если вы не укажете иначе, Nothing <: R <: Any).
Вот почему я отказался от идеи аргумента типа по умолчанию: я не мог найти способ сделать ее интуитивной. В этой системе ограничивающих диапазонов, как вы реализуете невысокий приоритет по умолчанию? Или, имеет ли приоритет по умолчанию "выбрать самый низкий тип", если он находится в допустимом диапазоне? Я не мог придумать решение, которое бы не путало, по крайней мере, в некоторых случаях. Если вы можете, пожалуйста, сообщите мне, поскольку мне очень интересно.
edit: Обратите внимание, что логика меняется на противоположные для контравариантных параметров. Поэтому, если у вас есть "класс D [-Q]", и вы говорите "новый D", вы получаете D [Any].
Ответ 3
Один из вариантов состоит в том, чтобы переместить вызов CanBuildFrom
в место, где он (или, точнее, его экземпляры) может помочь определить S
,
case class Generator[T,S](a: T, b: T, c: T)(implicit bf: CanBuildFrom[S, T, S]) {
def generate : S =
bf() += (a, b, c) result
}
Пример сеанса REPL,
scala> val g2 = Generator('a', 'b', 'c')
g2: Generator[Char,String] = Generator(a,b,c)
scala> g2.generate
res0: String = abc
Обновление
GeneratorFactory
также нужно будет изменить так, чтобы его метод build
распространял соответствующий экземпляр CanBuildFrom
на конструктор Generator
,
case class GeneratorFactory[T]() {
def build[S](seq: S)(implicit conv: S => Seq[T], bf: CanBuildFrom[S, T, S]) =
Generator[T, S](seq(0), seq(1), seq(2))
}
Не то, что с Scala < 2.10.0 вы не можете смешивать границы представлений и неявные списки параметров в том же определении метода, поэтому нам нужно перевести границу S <% Seq[T]
в ее эквивалентный неявный параметр S => Seq[T]
.