O que <: <, <% <e =: = significam no Scala 2.8 e onde eles estão documentados?

201

Eu posso ver nos documentos da API do Predef que são subclasses de um tipo de função genérica (From) => To, mas é tudo o que diz. Hum o que? Talvez haja documentação em algum lugar, mas os mecanismos de pesquisa não lidam com "nomes" como "<: <" muito bem, então não consegui encontrá-la.

Pergunta de acompanhamento: quando devo usar esses símbolos / classes descolados e por quê?

Jeff
fonte
6
Aqui está uma pergunta relacionada que pode responder à sua pergunta pelo menos parcialmente: stackoverflow.com/questions/2603003/operator-in-scala
Yardena
13
symbolhound.com é o seu código de pesquisa amigo :)
ron
Os typeclasses de Haskell realizam o trabalho desses operadores? Exemplo compare :: Ord a => a -> a -> Ordering:? Estou tentando entender esse conceito da Scala em relação à sua contraparte Haskell.
Kevin Meredith

Respostas:

217

Estes são chamados de restrições de tipo generalizado . Eles permitem que você, dentro de uma classe ou característica com parâmetros de tipo, restrinja ainda mais um de seus parâmetros de tipo. Aqui está um exemplo:

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
}

O argumento implícito evidenceé fornecido pelo compilador, se Afor String. Você pode pensar nisso como uma prova de que Aé String--o argumento em si não é importante, sabendo apenas que ele existe. [editar: bem, tecnicamente é realmente importante porque representa uma conversão implícita de Apara String, que é o que permite que você chame a.lengthe não faça o compilador gritar com você]

Agora eu posso usá-lo assim:

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

Mas se eu tentasse usá-lo com um Foocontendo algo diferente de um String:

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

Você pode ler esse erro como "não foi possível encontrar evidências de que Int == String" ... é como deveria ser! getStringLengthestá impondo mais restrições ao tipo do Aque o que Foogeralmente exige; ou seja, você só pode chamar getStringLengthem a Foo[String]. Essa restrição é imposta em tempo de compilação, o que é legal!

<:<e <%<funcionam da mesma forma, mas com pequenas variações:

  • A =:= B significa que A deve ser exatamente B
  • A <:< Bsignifica que A deve ser um subtipo de B (análogo à restrição de tipo simples<: )
  • A <%< Bsignifica que A deve ser visualizado como B, possivelmente via conversão implícita (análoga à restrição de tipo simples <%)

Esse trecho de @retronym é uma boa explicação de como esse tipo de coisa costumava ser realizado e como restrições de tipo generalizadas facilitam agora.

TERMO ADITIVO

Para responder à sua pergunta de acompanhamento, é certo que o exemplo que dei é bastante artificial e não é obviamente útil. Mas imagine usá-lo para definir algo como um List.sumIntsmétodo, que adiciona uma lista de números inteiros. Você não deseja permitir que esse método seja chamado em nenhum antigo List, apenas a List[Int]. No entanto, o Listconstrutor de tipos não pode ser tão restrito; você ainda deseja ter listas de strings, foos, barras e outras coisas. Portanto, ao colocar uma restrição de tipo generalizada sumInts, você pode garantir que apenas esse método tenha uma restrição adicional de que ele só pode ser usado em a List[Int]. Essencialmente, você está escrevendo um código de caso especial para certos tipos de listas.

Tom Crockett
fonte
3
Bem, ok, mas também existem métodos com os mesmos nomes Manifest, que você não mencionou.
Daniel C. Sobral
3
Os métodos em Manifestsão <:<e >:>única ... desde OP mencionado exatamente as 3 variedades de restrições de tipo generalizadas, eu estou supondo que é o que ele estava interessado.
Tom Crockett
12
@IttayD: é bastante inteligente ... class =:=[From, To] extends From => To, o que significa que um valor implícito do tipo From =:= Toé na verdade uma conversão implícita de Frompara To. Então, ao aceitar um parâmetro implícito do tipo, A =:= Stringvocê está dizendo que Apode ser implicitamente convertido em String. Se você alterasse a ordem e tornasse o argumento implícito do tipo String =:= A, não funcionaria, pois seria uma conversão implícita de Stringpara A.
Tom Crockett
25
Esses símbolos de três caracteres têm nomes? Meu problema com a sopa de símbolos de Scala é que eles são difíceis de falar verbalmente e é praticamente impossível usar o Google ou qualquer outro mecanismo de pesquisa para encontrar discussões e exemplos de seu uso.
Gigatron 26/03
4
@ Andrea Nope, isso só funcionará se os tipos forem exatamente iguais. Observe que eu disse que um valor implícito do tipo From =:= Tono escopo implica que você tem uma conversão implícita From => To, mas a implicação não é executada ao contrário; tendo uma conversão implícita A => Bé que não significa que você tem uma instância A =:= B. =:=é uma classe abstracta selado definido em scala.Predef, e tem apenas uma instância exposta ao público, o que está implícito, e é do tipo A =:= A. Então, você tem a garantia de que um valor implícito do tipo A =:= Btestemunhas do fato de que Ae Bsão iguais.
Tom Crockett
55

Não é uma resposta completa (outros já responderam a isso), só queria observar o seguinte, o que talvez ajude a entender melhor a sintaxe: A maneira como você normalmente usa esses "operadores", como por exemplo no exemplo do pelotom:

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

faz uso da sintaxe de infixo alternativa do Scala para operadores de tipo .

Portanto, A =:= Stringé o mesmo que =:=[A, String](e =:=é apenas uma classe ou característica com um nome atraente). Observe que essa sintaxe também funciona com classes "regulares", por exemplo, você pode escrever:

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

como isso:

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

É semelhante às duas sintaxes para chamadas de método, o "normal" com .e ()e a sintaxe do operador.

Jesper
fonte
2
necessidades upvote porque makes use of Scala's alternative infix syntax for type operators.faltando totalmente esta explicação, sem que a coisa toda não faz sentido
Ovidiu Dolha
39

Leia as outras respostas para entender quais são essas construções. Aqui é quando você deve usá-los. Você os utiliza quando precisa restringir um método apenas para tipos específicos.

Aqui está um exemplo. Suponha que você queira definir um par homogêneo, assim:

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

Agora você deseja adicionar um método smaller, como este:

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

Isso só funciona se Tfor solicitado. Você pode restringir a classe inteira:

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

Mas isso parece uma vergonha - pode haver usos para a classe quando Tnão for solicitado. Com uma restrição de tipo, você ainda pode definir o smallermétodo:

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

Não há problema em instanciar, digamos, a Pair[File], contanto que você não o invoque smaller .

No caso de Option, os implementadores queriam um orNullmétodo, mesmo que não faça sentido Option[Int]. Usando uma restrição de tipo, tudo está bem. Você pode usar orNullum Option[String], e pode formar um Option[Int]e usá-lo, desde que não o invoque orNull. Se você tentar Some(42).orNull, você recebe a mensagem encantadora

 error: Cannot prove that Null <:< Int
cayhorstmann
fonte
2
Percebo que isso ocorre anos depois dessa resposta, mas estou procurando por casos de uso <:<e acho que o Orderedexemplo não é mais tão convincente, já que agora você prefere usar a Orderingclasse tipográfica do que a Orderedcaracterística. Algo como: def smaller(implicit ord: Ordering[T]) = if (ord.lt(first, second)) first else second.
ebruchez
1
@ebruchez: um caso de uso é para codificar tipos de união em scala não modificada, consulte milessabin.com/blog/2011/06/09/scala-union-types-curry-howard
17

Depende de onde eles estão sendo usados. Na maioria das vezes, quando usadas ao declarar tipos de parâmetros implícitos, são classes. Eles também podem ser objetos em casos raros. Finalmente, eles podem ser operadores em Manifestobjetos. Eles são definidos scala.Predefnos dois primeiros casos, embora não sejam particularmente bem documentados.

Elas são destinadas a fornecer uma maneira de testar a relação entre as classes, assim como <:e <%fazer, em situações em que este último não pode ser usado.

Quanto à pergunta "quando devo usá-los?", A resposta é que você não deveria, a menos que saiba que deveria. :-) EDIT : Ok, ok, aqui estão alguns exemplos da biblioteca. Em Either, você tem:

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

Em Option, você tem:

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

Você encontrará outros exemplos nas coleções.

Daniel C. Sobral
fonte
:-)mais um desses? E eu concordaria que sua resposta para "Quando devo usá-los?" aplica-se a muitas coisas.
Mike Miller
"Elas são destinadas a fornecer uma maneira de testar a relação entre as classes" <- demasiado geral para ser útil
Jeff
3
"Quanto à pergunta" quando devo usá-los? ", A resposta é que você não deveria, a menos que saiba que deveria." <- É por isso que estou perguntando. Eu gostaria de poder fazer essa determinação por mim mesmo.
Jeff