Perché Scala deduce il Bottom Type quando il parametro type non è specificato?
Mi chiedo se qualcuno potrebbe spiegare la regola di inferenza in questo caso particolare di seguito e, cosa più importante, è razionale / implicazione?
case class E[A, B](a: A) // class E
E(2) // E[Int,Nothing] = E(2)
Nota che avrei potuto scrivere E[Int](2). Ciò che mi importa è perché si deduce che il secondo tipo di parametro sia Nothing(cioè il tipo Bottom) invece di diciamo Anyper esempio? Perché e qual è il razionale / Implicazione?
Solo per dare un po 'di contesto, questo è correlato alla definizione di E e come funziona per Sinistra e Destra. Entrambi sono definiti secondo lo schema
final case class X[+A, +B](value: A) extends Either[A, B]
Dove si istanzia, diciamo come Right[Int](2)e il tipo dedotto è Right[Nothing, Int]e per estensioneEither[Nothing, Int]
EDIT1
C'è coerenza qui, ma riesco ancora a capire il razionale. Di seguito è riportata la stessa definizione con un parametro controvarianza:
case class E[A, -B](a: A)// class E
E(2) // E[Int, Any] = E(2)
Quindi abbiamo la stessa cosa al contrario quando è contro-variante, e questo rende coerente la regola di tutto il comportamento o l'inferenza. Tuttavia il razionale per questo non sono sicuro ....
Perché non la regola opposta, ovvero inferire Anyquando Co-Variante / Invariante e Nothingquando Contra-Variante?
EDIT2
Alla luce di @slouc Answer, che ha un buon senso, non mi resta che capire cosa e perché il compilatore sta facendo quello che sta facendo. L'esempio seguente illustra la mia confusione
val myleft = Left("Error") // Left[String,Nothing] = Left(Error)
myleft map { (e:Int) => e * 4} // Either[String,Int] = Left(Error)
- Per prima cosa il compilatore aggiusta il tipo a qualcosa che "sicuramente funziona" per riutilizzare la conclusione di @slouc (anche se ha più senso nel contesto di una funzione)
Left[String,Nothing] - Successivamente la compilazione deduce
myleftche sia di tipo Either [String, Int]
data la definizione della mappa def map[B](f: A => B): Either[E, B], (e:Int) => e * 4può essere fornita solo se myleftè effettivamente Left[String,Int]oEither[String,Int]
Quindi, in altre parole, la mia domanda è: qual è il punto di fissare il tipo Nothingse deve cambiarlo in seguito.
Infatti quanto segue non viene compilato
val aleft: Left[String, Nothing] = Left[String, Int]("Error")
type mismatch;
found : scala.util.Left[String,Int]
required: Left[String,Nothing]
val aleft: Left[String, Nothing] = Left[String, Int]("Error")
Quindi perché dovrei dedurre un tipo, che normalmente mi impedirebbe di fare qualsiasi altra cosa su una variabile di quel tipo ( ma di sicuro funziona in termini di inferenza ), per cambiare infine quel tipo, quindi posso fare qualcosa con una variabile di quello tipo dedotto.
EDIT3
Edit2 è un po 'malinteso e tutto è chiarito nella risposta e nei commenti di @slouc.
Risposte
Covarianza:
dato il tipoF[+A]e la relazioneA <: B, vale quanto segue:F[A] <: F[B]Contravarianza:
dato il tipoF[-A]e la relazioneA <: B, vale quanto segue:F[A] >: F[B]
Se il compilatore non può dedurre il tipo esatto, risolverà il tipo più basso possibile in caso di covarianza e il tipo più alto possibile in caso di controvarianza.
Perché?
Questa è una regola molto importante quando si tratta di varianza nella sottotipizzazione. Può essere mostrato nell'esempio del seguente tipo di dati da Scala:
trait Function1[Input-, Output+]
In generale, quando un tipo è inserito nei parametri di funzione / metodo, significa che è nella cosiddetta "posizione controvariante". Se viene utilizzato nei valori di ritorno di una funzione / metodo, si trova nella cosiddetta "posizione covariante". Se è in entrambi, allora è invariante.
Ora, viste le regole dall'inizio di questo post, concludiamo che, dato:
trait Food
trait Fruit extends Food
trait Apple extends Fruit
def foo(someFunction: Fruit => Fruit) = ???
possiamo fornire
val f: Food => Apple = ???
foo(f)
La funzione fè un valido sostituto someFunctionperché:
Foodè un supertipo diFruit(controvarianza di input)Appleè un sottotipo diFruit(covarianza dell'output)
Possiamo spiegarlo in un linguaggio naturale in questo modo:
"Metodo
fooha bisogno di una funzione che può prendere unFruite produrre unFruitQuesto mezzo.fooAvranno un certoFruite avrà bisogno di una funzione che può alimentare a, e si aspettano un po 'diFruitschiena Se diventa una funzione.Food => Apple, Tutto è andato bene - può ancora dargli da mangiareFruit( perché la funzione prende qualsiasi cibo), e può ricevereFruit(le mele sono frutti, quindi il contratto è rispettato).
Tornando al tuo dilemma iniziale, si spera che questo spieghi perché, senza alcuna informazione aggiuntiva, il compilatore ricorra al tipo più basso possibile per i tipi covarianti e al tipo più alto possibile per quelli controvarianti. Se vogliamo fornire una funzione di foo, c'è uno che sappiamo funziona sicuramente: Any => Nothing.
Varianza in generale .
Varianza nella documentazione di Scala .
Articolo sulla varianza in Scala (divulgazione completa: l'ho scritto io).
MODIFICARE:
Penso di sapere cosa ti confonde.
Quando crei un'istanza di a Left[String, Nothing], puoi utilizzarlo successivamente mapcon una funzione Int => Whatever, o String => Whatever, o Any => Whatever. Ciò è precisamente a causa della controvarianza dell'input della funzione spiegato in precedenza. Ecco perché le tue mapopere.
"che senso ha fissare il tipo su Nothing se deve cambiarlo in seguito?"
Penso che sia un po 'difficile girare la testa intorno al compilatore che fissa il tipo sconosciuto Nothingin caso di controvarianza. Quando corregge il tipo sconosciuto Anyin caso di covarianza, sembra più naturale (può essere "Qualsiasi cosa"). A causa della dualità di covarianza e controvarianza spiegata in precedenza, lo stesso ragionamento si applica per controvariante Nothinge covariante Any.
Questa è una citazione da Unification of Compile-Time and Runtime Metaprogramming in Scala di Eugene Burmako
https://infoscience.epfl.ch/record/226166 (pagg. 95-96)
Durante l'inferenza del tipo, il controllore di tipo raccoglie i vincoli sugli argomenti di tipo mancanti dai limiti dei parametri di tipo, dai tipi di argomenti dei termini e persino dai risultati della ricerca implicita (l'inferenza del tipo funziona insieme alla ricerca implicita perché Scala supporta un analogo delle dipendenze funzionali). Si possono vedere questi vincoli come un sistema di disuguaglianze in cui gli argomenti di tipo sconosciuto sono rappresentati come variabili di tipo e l'ordine è imposto dalla relazione di sottotipizzazione.
Dopo aver raccolto i vincoli, il tipechecker avvia un processo graduale che, ad ogni passaggio, cerca di applicare una certa trasformazione alle disuguaglianze, creando un sistema di disuguaglianze equivalente, ma apparentemente più semplice. L'obiettivo dell'inferenza di tipo è trasformare le disuguaglianze originali in uguaglianze che rappresentano una soluzione unica del sistema originale.
La maggior parte delle volte l'inferenza del tipo riesce. In tal caso, gli argomenti di tipo mancanti vengono dedotti ai tipi rappresentati dalla soluzione.
Tuttavia, a volte l'inferenza del tipo non riesce. Ad esempio, quando un parametro di tipo
Tè fantasma, cioè non utilizzato nel termine parametri del metodo, la sua unica voce nel sistema di disuguaglianze saràL <: T <: U, doveLeUsono rispettivamente il suo limite inferiore e superiore. SeL != U, questa disuguaglianza non ha una soluzione univoca, ciò significa un fallimento dell'inferenza di tipo.Quando l'inferenza di tipo fallisce, cioè quando non è in grado di eseguire ulteriori passaggi di trasformazione e il suo stato di lavoro contiene ancora alcune disuguaglianze, il tipechecker rompe lo stallo. Prende tutti gli argomenti di tipo non ancora dedotti, cioè quelli le cui variabili sono ancora rappresentate da disuguaglianze, e li minimizza forzatamente , cioè li equipara ai loro limiti inferiori. Ciò produce un risultato in cui alcuni argomenti di tipo vengono dedotti con precisione e alcuni vengono sostituiti con tipi apparentemente arbitrari. Ad esempio, vengono dedotti parametri di tipo non vincolati
Nothing, che è una fonte comune di confusione per i principianti di Scala.
Puoi saperne di più sull'inferenza del tipo in Scala:
Hubert Plociniczak Decrittografia inferenza di tipo locale https://infoscience.epfl.ch/record/214757
Guillaume Martres Scala 3, Type Inference and You! https://www.youtube.com/watch?v=lMvOykNQ4zs
Guillaume Martres Dotty e tipi: la storia fino ad ora https://www.youtube.com/watch?v=YIQjfCKDR5A
Diapositive http://guillaume.martres.me/talks/
Aleksander Boruch-Gruszecki GADT a Dotty https://www.youtube.com/watch?v=VV9lPg3fNl8