¿Qué significan <: <, <% <y =: = en Scala 2.8, y dónde están documentados?

201

Puedo ver en los documentos de API para Predef que son subclases de un tipo de función genérica (From) => To, pero eso es todo lo que dice. ¿Um que? Tal vez hay documentación en alguna parte, pero los motores de búsqueda no manejan muy bien los "nombres" como "<: <", por lo que no he podido encontrarlo.

Pregunta de seguimiento: ¿cuándo debo usar estos símbolos / clases originales y por qué?

Jeff
fuente
66
Aquí hay una pregunta relacionada que puede responder a su pregunta al menos parcialmente: stackoverflow.com/questions/2603003/operator-in-scala
Yardena
13
symbolhound.com es tu amigo de búsqueda de código :)
ron
¿Haskell's typeclasses realiza el trabajo de estos operadores? Ejemplo: compare :: Ord a => a -> a -> Ordering? Estoy tratando de entender este concepto de Scala con respecto a su contraparte de Haskell.
Kevin Meredith

Respuestas:

217

Estos se denominan restricciones de tipo generalizadas . Le permiten, desde dentro de una clase o rasgo con parámetros de tipo, restringir aún más uno de sus parámetros de tipo. Aquí hay un ejemplo:

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
}

El evidencecompilador proporciona el argumento implícito , iff Aes String. Se puede pensar en él como una prueba de que Aes String--el argumento en sí mismo no es importante, sólo el saber que existe. [edit: bueno, técnicamente en realidad es importante porque representa una conversión implícita de Aa String, que es lo que te permite llamar a.lengthy no que el compilador te grite]

Ahora puedo usarlo así:

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

Pero si intenté usarlo con un Foocontenido que no sea un String:

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

Puede leer ese error como "no se pudo encontrar evidencia de que Int == String" ... ¡así es como debería ser! getStringLengthestá imponiendo restricciones en el tipo de Alo que Fooen general requiere; a saber, solo puede invocar getStringLengthen un Foo[String]. Esta restricción se aplica en tiempo de compilación, ¡lo cual es genial!

<:<y <%<funcionan de manera similar, pero con ligeras variaciones:

  • A =:= B significa que A debe ser exactamente B
  • A <:< Bsignifica que A debe ser un subtipo de B (análogo a la restricción de tipo simple<: )
  • A <%< Bsignifica que A debe ser visible como B, posiblemente mediante conversión implícita (análoga a la restricción de tipo simple <%)

Este fragmento de @retronym es una buena explicación de cómo se lograba este tipo de cosas y cómo las restricciones de tipo generalizadas lo hacen más fácil ahora.

APÉNDICE

Para responder a su pregunta de seguimiento, es cierto que el ejemplo que di es bastante artificial y obviamente no es útil. Pero imagine usarlo para definir algo así como un List.sumIntsmétodo, que suma una lista de enteros. No desea permitir que este método se invoque en ningún antiguo List, solo a List[Int]. Sin embargo, el Listconstructor de tipos no puede ser tan limitado; aún desea poder tener listas de cadenas, foos, barras y demás. Por lo tanto, al colocar una restricción de tipo generalizada sumInts, puede asegurarse de que solo ese método tenga una restricción adicional que solo se pueda usar en un List[Int]. Esencialmente, está escribiendo un código de caso especial para ciertos tipos de listas.

Tom Crockett
fuente
3
Bueno, está bien, pero también hay métodos con los mismos nombres Manifest, que no mencionaste.
Daniel C. Sobral
3
Los métodos Manifestestán en <:<y >:>solo ... ya que OP mencionó exactamente las 3 variedades de restricciones de tipo generalizadas, supongo que eso era lo que le interesaba.
Tom Crockett
12
@IttayD: es bastante inteligente ... class =:=[From, To] extends From => To, lo que significa que un valor implícito de tipo From =:= Toes en realidad una conversión implícita de Froma To. Entonces, al aceptar un parámetro implícito de tipo A =:= String, estás diciendo que Ase puede convertir implícitamente a String. Si cambia el orden y hace que el argumento implícito sea de tipo String =:= A, no funcionaría, porque sería una conversión implícita de Stringa A.
Tom Crockett
25
¿Los símbolos de tres caracteres tienen nombres? Mi problema con la sopa de símbolos de Scala es que es difícil hablarles verbalmente, y es prácticamente imposible usar Google o cualquier otro motor de búsqueda para encontrar discusiones y ejemplos de su uso.
Gigatron
44
@Andrea No, esto solo funcionará si los tipos son exactamente iguales. Tenga en cuenta que dije que tener un valor implícito de tipo From =:= Toen alcance implica que tiene una conversión implícita From => To, pero la implicación no se ejecuta al revés; tener una conversión implícita A => Bno no implica que tiene una instancia de A =:= B. =:=es una clase abstracta sellada definida enscala.Predef , y tiene solo una instancia expuesta públicamente, que es implícita y es de tipo A =:= A. Lo que está garantizado que un valor implícito del tipo A =:= Btestigos del hecho de que Ay Bson iguales.
Tom Crockett
55

No es una respuesta completa (otros ya han respondido esto), solo quería señalar lo siguiente, que tal vez ayude a comprender mejor la sintaxis: la forma en que normalmente usa estos "operadores", como por ejemplo en el ejemplo de pelotom:

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

hace uso de la sintaxis infija alternativa de Scala para operadores de tipo .

Entonces, A =:= Stringes lo mismo que =:=[A, String](y =:=es solo una clase o rasgo con un nombre elegante). Tenga en cuenta que esta sintaxis también funciona con clases "regulares", por ejemplo, puede escribir:

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

Me gusta esto:

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

Es similar a las dos sintaxis para llamadas a métodos, la sintaxis "normal" con .y ()y el operador.

Jesper
fuente
2
necesita un voto positivo porque makes use of Scala's alternative infix syntax for type operators.falta totalmente esta explicación sin la cual todo esto no tiene sentido
Ovidiu Dolha
39

Lea las otras respuestas para comprender cuáles son estos constructos. Aquí es cuando debes usarlos. Los usa cuando necesita restringir un método solo para tipos específicos.

Aquí hay un ejemplo. Suponga que desea definir un par homogéneo, así:

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

Ahora desea agregar un método smaller, como este:

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

Eso solo funciona si Tse ordena. Puedes restringir toda la clase:

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

Pero eso parece una pena, podría haber usos para la clase cuando Tno se ordena. Con una restricción de tipo, aún puede definir el smallermétodo:

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

Está bien instanciar, digamos, a Pair[File], siempre y cuando no llames smaller .

En el caso de Option, los implementadores querían un orNullmétodo, aunque no tiene sentido Option[Int]. Al usar una restricción de tipo, todo está bien. Puede usarlo orNullen Option[String], y puede formarlo Option[Int]y usarlo, siempre que no lo llame orNull. Si lo intentas Some(42).orNull, obtienes el mensaje encantador

 error: Cannot prove that Null <:< Int
cayhorstmann
fuente
2
Me doy cuenta de que esto es años después de esta respuesta, pero estoy buscando casos de uso <:<, y creo que el Orderedejemplo ya no es tan convincente ya que ahora preferiría usar la Orderingclase de tipos en lugar del Orderedrasgo. Algo así como: def smaller(implicit ord: Ordering[T]) = if (ord.lt(first, second)) first else second.
ebruchez
1
@ebruchez: un caso de uso es para codificar tipos de unión en scala no modificada, consulte milessabin.com/blog/2011/06/09/scala-union-types-curry-howard
17

Depende de dónde se estén utilizando. Muy a menudo, cuando se usan al declarar tipos de parámetros implícitos, son clases. También pueden ser objetos en casos excepcionales. Finalmente, pueden ser operadores de Manifestobjetos. Se definen dentro scala.Predefde los dos primeros casos, aunque no están particularmente bien documentados.

Su objetivo es proporcionar una forma de probar la relación entre las clases, al igual que <:y <%en las situaciones en que estas últimas no se pueden utilizar.

En cuanto a la pregunta "¿cuándo debería usarlos?", La respuesta es que no debería, a menos que sepa que debería hacerlo. :-) EDITAR : Ok, ok, aquí hay algunos ejemplos de la biblioteca. En Either, tienes:

/**
  * 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)
 }

En Option, tienes:

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

Encontrarás otros ejemplos en las colecciones.

Daniel C. Sobral
fuente
¿Es :-)otro de estos? Y estaría de acuerdo en que su respuesta a "¿Cuándo debería usarlos?" se aplica a muchas cosas.
Mike Miller
"Están destinados a proporcionar una manera de probar la relación entre las clases" <- demasiado general para ser útil
Jeff
3
"En cuanto a la pregunta" ¿cuándo debo usarlos? ", La respuesta es que no debería, a menos que sepa que debería". <- Por eso te pregunto. Me gustaría poder tomar esa determinación por mí mismo.
Jeff