2016-01-07 7 views
5

Si consideri il seguente:Passando intorno tracciato di testo dipendente non riesce a mantenere il valore dipendente

trait Platform { 
    type Arch <: Architecture 
    def parseArch(str: String): Option[Arch] 
} 

object Platform { 
    def parse(str: String): Option[Platform] = ??? 
} 

trait Architecture 

def main() { 
    def exec(p: Platform)(a: p.Arch) = ??? 

    Platform.parse("ios") 
    .flatMap(p => p.parseArch("arm64").map(a => (p, a))) 
    .flatMap { case (p, a) => exec(p)(a) } // <----- This fails to compile 
} 

exec(p)(a) non riesce a compilare con messaggio di errore:

Error:(17, 40) type mismatch;
found : a.type (with underlying type A$A2.this.Platform#Arch)
required: p.Arch .flatMap { case (p, a) => exec(p)(a) }

dal messaggio di errore, sembra che scalac non riesce a mantenere il valore (p) su cui dipende Arch e quindi scegliere di tipo proiezione invece (anche se non sono troppo sicuro di cosa significa A$A2.this).

Per quello che vale, sostituendo l'ultima riga con la seguente compilerà:

.flatMap(p => exec(p)(p.parseArch("arm64").get)) 

Si tratta di una limitazione nel compilatore Scala o forse mi manca qualcosa qui?

risposta

4

La soluzione semplice

La cosa migliore quando si tratta di tipi di percorso-dipendente è quello di mantenere sempre il valore proprietario in giro, perché Scala ha inferenza molto limitato e capacità di ragionamento altrimenti.

Ad esempio, l'esempio di codice potrebbe essere riscritto come:

Platform.parse("ios") flatMap { 
    p => p.parseArch("arm64").map(exec(p)) 
} 

E 'generalmente possibile eseguire tali riscritture, anche se il codice viene spesso diventano meno conciso ed elegante. Una pratica comune è usare le funzioni dipendenti e le classi parametriche.

Utilizzando tipi dipendenti

Nel tuo esempio, il codice:

Platform.parse("ios").flatMap(p => p.parseArch("arm64").map(a => (p, a))) 

è di tipo Option[(Platform, Platform#Arch)], perché l'inferenza di Scala non può mantenere il fatto che secondo elemento della tupla dipende dal primo elemento. (Si ottiene A$A2.this.Platform perché si è dichiarato Platform in un contesto interno.)

In altre parole, il tipo di Scala Tuple2 non dipende. Si potrebbe correggere tale da rendere la nostra propria classe:

case class DepPair(p: Platform)(a: p.Arch) 

Tuttavia, Scala non supporta ancora le firme di classe dipendenti, e non si compila. Invece, abbiamo impostato per utilizzare un tratto:

trait Dep { 
    val plat: Platform 
    val arch: plat.Arch 
} 
Platform.parse("ios") 
    .flatMap { p => p.parseArch("arm64").map { a => 
    new Dep { val plat: p.type = p; val arch: p.Arch = a }}} 
    .flatMap { dep => exec(dep.plat)(dep.arch) } 

Nota le attribuzioni sul val plat e val arch, come senza di loro, Scala cercherà di dedurre un tipo raffinato che renderà tipo controllo sicuro.

Siamo di fatto al limite di ciò che è ragionevole fare in Scala (IMHO). Ad esempio, se avessimo parametrizzata trait Dep[P <: Platform], avremmo ottenuto in tutti i tipi di problemi.In particolare:

Error:(98, 15) type mismatch; 
found : Platform => Option[Dep[p.type]] forSome { val p: Platform } 
required: Platform => Option[B] 

Scala deduce un tipo di funzione esistenziale, ma quello che vorremmo è in realtà per avere la quantificazione esistenziale all'interno il tipo di funzione. Dobbiamo guidare Scala per capire che, e si finisce con qualcosa di simile:

Platform.parse("ios").flatMap[Dep[p.type] forSome { val p: Platform }]{ 
    case p => p.parseArch("arm64").map{case a: p.Arch => 
     new Dep[p.type] { val plat: p.type = p; val arch = a }}} 
    .flatMap { dep => exec(dep.plat)(dep.arch) } 

Ora ti permetterà di decidere da che parte è la migliore: il bastone con il proprietario val intorno (soluzione semplice), o il rischio perdendo il senso di sanità mentale che ti era rimasto!

Ma parlando di perdere la sanità mentale e esistenziali, cerchiamo di indagare un po 'più ...

Uso esistenziali (fallito)

Il tipo problematica del risultato intermedio nel codice era Option[(Platform, Platform#Arch)]. V'è in realtà un modo per esprimere al meglio, utilizzando un esistenziali, come in:

Option[(p.type, p.Arch) forSome {val p: Platform}] 

Possiamo aiutare Scala specificando esplicitamente, in modo che il risultato intermedio ha il tipo di destinazione:

val tmp: Option[(p.type, p.Arch) forSome {val p: Platform}] = 
    Platform.parse("ios") 
    .flatMap { case p => p.parseArch("arm64").map { a => (p, a): (p.type, p.Arch) }} 

Tuttavia, ora tocchiamo un'area molto sensibile del sistema di tipi di Scala, e spesso causerà problemi. In realtà, non ho trovato un modo per esprimere la seconda flatMap ...

Cercando tmp.flatMap { case (p, a) => exec(p)(a) } dà la molto utile:

Error:(30, 30) type mismatch; 
found : a.type (with underlying type p.Arch) 
required: p.Arch 

Un altro studio:

tmp.flatMap { 
    (tup: (p.type, p.Arch) forSome {val p: Platform}) => exec(tup._1)(tup._2) 
} 
Error:(32, 79) type mismatch; 
found : tup._2.type (with underlying type p.Arch) 
required: tup._1.Arch 

A questo punto, penso che ogni individuo ragionevole si arrenderebbe e probabilmente starà lontano dalla programmazione di Scala per alcuni giorni ;-)

+0

Intersecando. Suppongo che i tipi di path-dependent siano ancora piuttosto approssimativi attorno ai bordi di scala. Dopo aver letto la tua risposta, ho rinunciato a trasmetterli in tuple (la soluzione 'trait' è interessante ma un po 'troppo prolissa) e invece ho fatto un tratto interiore per mantenere il riferimento al tratto esterno come mostrato di seguito. Grazie per aver approvato i miei dubbi. –

+0

Grande. Si noti che è possibile creare la propria piattaforma 'val'' a' def', in modo da non dover tenere un puntatore aggiuntivo al tratto esterno (Scala già memorizza i puntatori ai tratti esterni nei tratti interni). –

+0

Non conoscevo la differenza tra il puntatore 'def' e' val'. Non sarebbe "def" ugualmente creare un puntatore a qualunque cosa faccia riferimento alla sua _closure_ (non è sicuro se questa è una cosa in scala)? Forse potresti condividere un link? –

3

ho imparato a riconoscere la limitazione di corrente di compilatore Scala (come mostrato dalla risposta LP s'), e invece si avvicinò con questa soluzione:

trait Platform { 
    trait Architecture { 
    val platform: Platform.this.type = Platform.this 
    } 

    object Architecture { 
    def parse(str: String): Option[Architecture] = ??? 
    } 
} 

object Platform { 
    def parse(str: String): Option[Platform] = ??? 
} 

def main() { 
    def exec(a: Platform#Architecture) = { 
    val p = a.platform 
    ??? 
    } 

    Platform.parse("ios") 
    .flatMap(p => p.parseArch("arm64")) 
    .flatMap(a => exec(a)) 
} 

Fortunatamente, tratto interiore può fare riferimento al tratto esterno in scala. In questo modo, non è necessario passare intorno allo p e allo p.Arch insieme, invece ogni a: Platform#Architecture detiene il riferimento al proprio p: Platform.