Подтвердить что ты не робот

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]

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

Любые идеи, как я могу это сделать?

4b9b3361

Ответ 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].