Что означают <: <, <% <, и =: = среднее значение в Scala 2.8, и где они задокументированы?

В документации по API для Predef я вижу, что они являются подклассами универсального типа функции (From) => To, но этим все сказано. Хм что? Может быть, где-то есть документация, но поисковые системы не очень хорошо обрабатывают такие "имена", как "<: <", поэтому я не смог их найти.

Дополнительный вопрос: когда я должен использовать эти классные символы/классы и почему?

Ответы

Ответ 1

Они называются обобщенными ограничениями типа. Они позволяют вам, из класса или свойства, параметризованного по типу, дополнительно ограничить один из его параметров типа. Вот пример:

case class Foo[A](a:A) { // 'A' can be substituted with any type
    // getStringLength can only be used if this is a Foo[String]
    def getStringLength(implicit evidence: A =:= String) = a.length
}

Неявный аргумент evidence предоставляется компилятором, iff A - String. Вы можете считать это доказательством того, что A - String - сам аргумент не важен, только зная, что он существует. [edit: ну, технически это действительно важно, потому что оно представляет собой неявное преобразование от A до String, что позволяет вам называть a.length и не иметь крик компилятора у вас]

Теперь я могу использовать его так:

scala> Foo("blah").getStringLength
res6: Int = 4

Но если я попытался использовать его с Foo, содержащим нечто, отличное от String:

scala> Foo(123).getStringLength
<console>:9: error: could not find implicit value for parameter evidence: =:=[Int,String]

Вы можете прочитать эту ошибку как "не смогли найти доказательства, что Int == String"... это как и должно быть! getStringLength налагает дополнительные ограничения на тип A, чем обычно требуется Foo; а именно, вы можете вызвать getStringLength только на Foo[String]. Это ограничение применяется во время компиляции, что очень круто!

<:< и <%< работают аналогично, но с небольшими вариациями:

  • A =:= B означает, что A должно быть точно B
  • A <:< B означает, что A должен быть подтипом B (аналогичным простому типу <:)
  • A <%< B означает, что A должен быть доступен для просмотра как B, возможно через неявное преобразование (аналогично простому типу <%)

Этот фрагмент от @retronym является хорошим объяснением того, как это делалось раньше и как обобщенные ограничения типов облегчают сейчас.

ДОПОЛНЕНИЕ

Чтобы ответить на ваш последующий вопрос, по общему признанию, пример, который я дал, довольно надуманный и явно не полезный. Но представьте, используя его, чтобы определить что-то вроде метода List.sumInts, который добавляет список целых чисел. Вы не хотите, чтобы этот метод вызывался в любом старом List, просто List[Int]. Однако конструктор типа List не может быть так ограничен; вы все равно хотите иметь списки строк, foos, bars и whatnots. Поэтому, помещая ограничение обобщенного типа на sumInts, вы можете убедиться, что только этот метод имеет дополнительное ограничение, которое можно использовать только в List[Int]. По сути, вы пишете специальный код для определенных видов списков.

Ответ 2

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

def getStringLength(implicit evidence: A =:= String)

использует Scala альтернативный синтаксис infix для операторов типа.

Итак, A =:= String совпадает с =:=[A, String]=:= - это просто класс или признак с фантастическим именем). Обратите внимание, что этот синтаксис также работает с "обычными" классами, например, вы можете написать:

val a: Tuple2[Int, String] = (1, "one")

вот так:

val a: Int Tuple2 String = (1, "one")

Он похож на два синтаксиса для вызовов методов: "нормальный" с . и () и синтаксис оператора.

Ответ 3

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

Вот пример. Предположим, вы хотите определить однородную пару, например:

class Pair[T](val first: T, val second: T)

Теперь вы хотите добавить метод smaller, например:

def smaller = if (first < second) first else second

Это работает, только если T упорядочено. Вы можете ограничить весь класс:

class Pair[T <: Ordered[T]](val first: T, val second: T)

Но это кажется позором - для класса можно использовать, когда T не упорядочен. С ограничением типа вы все равно можете определить метод smaller:

def smaller(implicit ev: T <:< Ordered[T]) = if (first < second) first else second

Это нормально, чтобы создать экземпляр, скажем, < <28 > , если вы не назовете smaller на нем.

В случае Option разработчикам нужен метод orNull, хотя это не имеет смысла для Option[Int]. Используя ограничение типа, все хорошо. Вы можете использовать orNull на Option[String], и вы можете создать Option[Int] и использовать его, если вы не вызываете orNull на нем. Если вы попробуете Some(42).orNull, вы получите очаровательное сообщение

 error: Cannot prove that Null <:< Int

Ответ 4

Это зависит от того, где они используются. Чаще всего при использовании при объявлении типов неявных параметров они являются классами. Они также могут быть объектами в редких случаях. Наконец, они могут быть операторами на объектах Manifest. Они определены внутри scala.Predef в первых двух случаях, хотя и не особенно хорошо документированы.

Они предназначены для того, чтобы дать возможность проверить взаимосвязь между классами, точно так же, как <: и <% do, в ситуациях, когда последнее не может быть использовано.

Что касается вопроса "когда я должен их использовать?", ответ не должен, если вы не знаете, что вам нужно.:-) EDIT: Хорошо, хорошо, вот несколько примеров из библиотеки. На Either у вас есть:

/**
  * Joins an <code>Either</code> through <code>Right</code>.
  */
 def joinRight[A1 >: A, B1 >: B, C](implicit ev: B1 <:< Either[A1, C]): Either[A1, C] = this match {
   case Left(a)  => Left(a)
   case Right(b) => b
 }

 /**
  * Joins an <code>Either</code> through <code>Left</code>.
  */
 def joinLeft[A1 >: A, B1 >: B, C](implicit ev: A1 <:< Either[C, B1]): Either[C, B1] = this match {
   case Left(a)  => a
   case Right(b) => Right(b)
 }

В Option у вас есть:

def orNull[A1 >: A](implicit ev: Null <:< A1): A1 = this getOrElse null

В сборниках вы найдете несколько других примеров.

Ответ 5

В Scala 2.13 они были Predef из Predef: Move <: <, =: =, DummyImplicits из Predef # 7350

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

... ограничить любой абстрактный тип T, находящийся в области видимости в списке аргументов метода (а не только параметры собственного типа метода)

Вот пример, иллюстрирующий аспект "не только параметров собственного типа метода". Скажем у нас

case class Foo[A, B](f: A => B) {
  def bar[C <: A](x: C)(implicit e: B <:< String): B = f(x)
}

Foo[Int, String](x => x.toString).bar(1) // OK.
Foo[Int, Double](x => x.toDouble).bar(1) // error: Cannot prove that Double <:< String.

Обратите внимание, как мы можем ограничить параметр типа B несмотря на тот факт, что он не отображается в предложении параметра типа bar [C <: A]. Если бы мы вместо этого попытались ограничить B в предложении параметра типа bar следующим образом

def bar[B <: String]

мы будем скрывать параметр типа B из области действия Foo[A, B]. Реальный пример этого из библиотеки будет toMap:

trait IterableOnceOps[+A, +CC[_], +C] extends Any {
  ...
  def toMap[K, V](implicit ev: A <:< (K, V)): immutable.Map[K, V] =
  ...
}