Cosa significano <: <, <% <e =: = in Scala 2.8 e dove sono documentati?


201

Posso vedere nei documenti API di Predef che sono sottoclassi di un tipo di funzione generica (Da) => A, ma è tutto ciò che dice. Ehm, cosa? Forse c'è documentazione da qualche parte, ma i motori di ricerca non gestiscono molto bene i "nomi" come "<: <", quindi non sono riuscito a trovarli.

Domanda di follow-up: quando dovrei usare questi simboli / classi funky, e perché?


6
Ecco una domanda correlata che potrebbe rispondere alla tua domanda almeno in parte: stackoverflow.com/questions/2603003/operator-in-scala
Yardena

13
symbolhound.com è il tuo amico per la ricerca del codice :)
ron,

Haskell's typeclasses svolge il lavoro di questi operatori? Esempio compare :: Ord a => a -> a -> Ordering:? Sto cercando di capire questo concetto di Scala rispetto alla sua controparte Haskell.
Kevin Meredith,

Risposte:


217

Questi sono chiamati vincoli di tipo generalizzati . Consentono all'utente, all'interno di una classe o caratteristica parametrizzata di tipo, di limitare ulteriormente uno dei suoi parametri di tipo. Ecco un esempio:

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
}

L'argomento implicito evidenceviene fornito dal compilatore, se Aè String. Si può pensare ad esso come un prova che Aè String--il argomento in sé non è importante, ma solo sapendo che esiste. [modifica: beh, tecnicamente in realtà è importante perché rappresenta una conversione implicita da Aa String, che è ciò che ti permette di chiamare a.lengthe non avere l'urlo del compilatore contro di te]

Ora posso usarlo in questo modo:

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

Ma se ho provato ad usarlo con un Fooqualcosa di diverso da un String:

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

Puoi leggere quell'errore come "impossibile trovare la prova che Int == String" ... è come dovrebbe essere! getStringLengthimpone ulteriori restrizioni al tipo di Aquelle Foogeneralmente richieste; vale a dire, puoi solo invocare getStringLengthsu a Foo[String]. Questo vincolo viene applicato in fase di compilazione, il che è fantastico!

<:<e <%<funziona in modo simile, ma con lievi variazioni:

  • A =:= B significa che A deve essere esattamente B
  • A <:< Bsignifica che A deve essere un sottotipo di B (analogo al vincolo di tipo semplice<: )
  • A <%< Bsignifica che A deve essere visualizzabile come B, possibilmente tramite conversione implicita (analogo al vincolo di tipo semplice <%)

Questo frammento di @retronym è una buona spiegazione di come questo genere di cose veniva realizzato e di come i vincoli di tipo generalizzati ora lo rendono più semplice.

ADDENDUM

Per rispondere alla tua domanda di follow-up, è vero che l'esempio che ho dato è piuttosto elaborato e ovviamente non utile. Ma immagina di usarlo per definire qualcosa di simile a un List.sumIntsmetodo, che aggiunge un elenco di numeri interi. Non vuoi permettere che questo metodo sia invocato su nessun vecchio List, solo a List[Int]. Tuttavia, il Listcostruttore del tipo non può essere così limitato; vuoi comunque essere in grado di avere elenchi di stringhe, foo, barre e quant'altro. Pertanto, impostando un vincolo di tipo generalizzato su sumInts, è possibile assicurarsi che proprio quel metodo abbia un vincolo aggiuntivo che può essere utilizzato solo su un List[Int]. In sostanza stai scrivendo un codice per casi speciali per alcuni tipi di elenchi.


3
Bene, ok, ma ci sono anche metodi con gli stessi nomi Manifest, che non hai menzionato.
Daniel C. Sobral,

3
I metodi su Manifestsono <:<e >:>solo ... dato che OP ha menzionato esattamente le 3 varietà di vincoli di tipo generalizzato, suppongo che sia quello che gli interessava.
Tom Crockett

12
@IttayD: è piuttosto intelligente ... class =:=[From, To] extends From => To, il che significa che un valore implicito di tipo From =:= Toè in realtà una conversione implicita da Froma To. Quindi accettando un parametro implicito di tipo A =:= Stringstai dicendo che Apuò essere implicitamente convertito in String. Se cambiassi l'ordine e rendessi l'argomento implicito di tipo String =:= A, non funzionerebbe, perché questa sarebbe una conversione implicita da Stringa A.
Tom Crockett,

25
Quei simboli di tre caratteri hanno nomi? Il mio problema con la zuppa di simboli di Scala è che sono difficili da parlare verbalmente ed è praticamente impossibile usare Google o qualsiasi altro motore di ricerca per trovare discussioni ed esempi del loro utilizzo.
Gigatron,

4
@Andrea No, funzionerà solo se i tipi sono esattamente uguali. Nota che ho detto che un valore implicito di tipo From =:= Tonell'ambito From => Torientra nella tua implicita conversione , ma l'implicazione non viene eseguita al contrario; avere una conversione implicita A => Bnon non implica di avere un'istanza di A =:= B. =:=è una classe astratta sigillata definita in scala.Predef, e ha solo un'istanza esposta pubblicamente, che è implicita ed è di tipo A =:= A. Quindi sei sicuro che un valore implicito di tipo A =:= Btestimonia il fatto che Ae Bsono uguali.
Tom Crockett,

55

Non una risposta completa (altri hanno già risposto a questa domanda), volevo solo notare quanto segue, che forse aiuta a capire meglio la sintassi: il modo in cui normalmente si usano questi "operatori", come ad esempio nell'esempio di pelotom:

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

utilizza la sintassi alternativa di Scala per gli operatori di tipo .

Quindi, A =:= Stringè lo stesso di =:=[A, String](ed =:=è solo una classe o un tratto con un nome di bell'aspetto). Nota che questa sintassi funziona anche con classi "normali", ad esempio puoi scrivere:

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

come questo:

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

È simile ai due sintassi per chiamate di metodo, il "normale" con .e ()e la sintassi dell'operatore.


2
ha bisogno di essere votato perché makes use of Scala's alternative infix syntax for type operators.manca totalmente questa spiegazione senza la quale l'intera cosa non ha senso
Ovidiu Dolha,

39

Leggi le altre risposte per capire quali sono questi costrutti. Ecco quando dovresti usarli. Li usi quando devi vincolare un metodo solo per tipi specifici.

Ecco un esempio Supponiamo di voler definire una coppia omogenea, in questo modo:

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

Ora vuoi aggiungere un metodo smaller, come questo:

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

Funziona solo se Tordinato. Potresti limitare l'intera classe:

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

Ma sembra un peccato - potrebbero esserci degli usi per la classe quando Tnon è ordinato. Con un vincolo di tipo, è ancora possibile definire il smallermetodo:

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

Va bene istanziare, diciamo, a Pair[File], purché non lo si faccia smaller .

Nel caso di Option, gli implementatori volevano un orNullmetodo, anche se non ha senso Option[Int]. Usando un vincolo di tipo, va tutto bene. Puoi usare orNullun Option[String], e puoi formare un Option[Int]e usarlo, purché tu non lo chiami orNull. Se ci provi Some(42).orNull, ricevi il messaggio affascinante

 error: Cannot prove that Null <:< Int

2
Mi rendo conto che sono trascorsi anni da questa risposta, ma sto cercando casi d'uso <:<, e penso che l' Orderedesempio non sia più così convincente poiché ora preferiresti usare la Orderingtypeclass piuttosto che il Orderedtratto. Qualcosa come: def smaller(implicit ord: Ordering[T]) = if (ord.lt(first, second)) first else second.
ebruchez,

1
@ebruchez: un caso d'uso è per la codifica dei tipi di unione in scala non modificata, vedere milessabin.com/blog/2011/06/09/scala-union-types-curry-howard

17

Dipende da dove vengono utilizzati. Molto spesso, se utilizzati durante la dichiarazione di tipi di parametri impliciti, sono classi. Possono essere anche oggetti in rari casi. Infine, possono essere operatori sugli Manifestoggetti. Sono definiti all'interno scala.Predefnei primi due casi, anche se non particolarmente ben documentati.

Hanno lo scopo di fornire un modo per testare la relazione tra le classi, proprio come <:e <%fare, in situazioni in cui quest'ultima non può essere utilizzata.

Per quanto riguarda la domanda "quando dovrei usarli?", La risposta è che non dovresti, a meno che tu non sappia che dovresti. :-) EDIT : Ok, ok, ecco alcuni esempi dalla libreria. Su Either, hai:

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

Su Option, hai:

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

Troverai altri esempi sulle collezioni.


È :-)un altro di questi? E sarei d'accordo che la tua risposta a "Quando dovrei usarli?" si applica a moltissime cose.
Mike Miller,

"Hanno lo scopo di fornire un modo per testare la relazione tra le classi" <- troppo generale per essere utile
Jeff

3
"Per quanto riguarda la domanda" quando dovrei usarli? ", La risposta è che non dovresti, a meno che tu non sappia che dovresti". <- Ecco perché te lo chiedo. Mi piacerebbe essere in grado di prendere quella determinazione per me stesso.
Jeff,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.