Risorse di programmazione di tipo Scala


102

Secondo questa domanda , il sistema di tipi di Scala è completo di Turing . Quali risorse sono disponibili che consentono a un nuovo arrivato di sfruttare la potenza della programmazione a livello di tipo?

Ecco le risorse che ho trovato finora:

Queste risorse sono ottime, ma sento che mi mancano le basi e quindi non ho una solida base su cui costruire. Ad esempio, dov'è un'introduzione alle definizioni di tipo? Quali operazioni posso eseguire sui tipi?

Ci sono buone risorse introduttive?


Personalmente, trovo ragionevole l'ipotesi che qualcuno che vuole fare programmazione a livello di tipo in Scala sappia già come fare programmazione in Scala abbastanza ragionevole. Anche se significa che non capisco una parola di quegli articoli a cui hai linkato :-)
Jörg W Mittag

Risposte:


140

Panoramica

La programmazione a livello di tipo presenta molte somiglianze con la programmazione a livello di valore tradizionale. Tuttavia, a differenza della programmazione a livello di valore, in cui il calcolo avviene in fase di esecuzione, nella programmazione a livello di tipo, il calcolo avviene in fase di compilazione. Cercherò di tracciare parallelismi tra la programmazione a livello di valore e la programmazione a livello di tipo.

paradigmi

Esistono due paradigmi principali nella programmazione a livello di tipo: "orientato agli oggetti" e "funzionale". La maggior parte degli esempi collegati da qui seguono il paradigma orientato agli oggetti.

Un buon esempio abbastanza semplice di programmazione a livello di tipo nel paradigma orientato agli oggetti può essere trovato nell'implementazione di apocalisp del lambda calcolo , replicata qui:

// Abstract trait
trait Lambda {
  type subst[U <: Lambda] <: Lambda
  type apply[U <: Lambda] <: Lambda
  type eval <: Lambda
}

// Implementations
trait App[S <: Lambda, T <: Lambda] extends Lambda {
  type subst[U <: Lambda] = App[S#subst[U], T#subst[U]]
  type apply[U] = Nothing
  type eval = S#eval#apply[T]
}

trait Lam[T <: Lambda] extends Lambda {
  type subst[U <: Lambda] = Lam[T]
  type apply[U <: Lambda] = T#subst[U]#eval
  type eval = Lam[T]
}

trait X extends Lambda {
  type subst[U <: Lambda] = U
  type apply[U] = Lambda
  type eval = X
}

Come si può vedere nell'esempio, il paradigma orientato agli oggetti per la programmazione a livello di tipo procede come segue:

  • Primo: definisci un tratto astratto con vari campi di tipo astratto (vedi sotto per cos'è un campo astratto). Questo è un modello per garantire che alcuni tipi di campi esistano in tutte le implementazioni senza forzare un'implementazione. Nell'esempio lambda calcolo, ciò corrisponde a trait Lambdache i seguenti tipi esistano garanzie: subst, applye eval.
  • Avanti: definire i sottotratti che estendono il tratto astratto e implementano i vari campi di tipo astratto
    • Spesso, questi sottotratti saranno parametrizzati con argomenti. Nell'esempio del lambda calcolo, i sottotipi sono trait App extends Lambdaparametrizzati con due tipi ( Se T, entrambi devono essere sottotipi di Lambda), trait Lam extends Lambdaparametrizzati con un tipo ( T) e trait X extends Lambda(che non è parametrizzato).
    • i campi di tipo sono spesso implementati facendo riferimento ai parametri di tipo del sottoritratto e talvolta facendo riferimento ai loro campi di tipo tramite l'operatore hash: #(che è molto simile all'operatore punto: .per i valori). Nel tratto Appdell'esempio lambda calcolo, il tipo evalviene realizzato nel modo seguente: type eval = S#eval#apply[T]. Si tratta essenzialmente di chiamare il evaltipo di parametro del tratto Se di chiamare applycon parametro Tsul risultato. Nota, Sè garantito che abbia un evaltipo perché il parametro specifica che è un sottotipo di Lambda. Allo stesso modo, il risultato di evaldeve avere un applytipo, poiché è specificato che è un sottotipo di Lambda, come specificato nel tratto astratto Lambda.

Il paradigma funzionale consiste nel definire molti costruttori di tipi parametrizzati che non sono raggruppati insieme in tratti.

Confronto tra programmazione a livello di valore e programmazione a livello di tipo

  • classe astratta
    • il valore di livello: abstract class C { val x }
    • tipo di livello: trait C { type X }
  • tipi dipendenti dal percorso
    • C.x (riferendosi al valore del campo / funzione x nell'oggetto C)
    • C#x (facendo riferimento al tipo di campo x nel tratto C)
  • firma della funzione (nessuna implementazione)
    • il valore di livello: def f(x:X) : Y
    • livello di tipo: type f[x <: X] <: Y(questo è chiamato "costruttore di tipo" e di solito si verifica nel tratto astratto)
  • implementazione della funzione
    • il valore di livello: def f(x:X) : Y = x
    • tipo di livello: type f[x <: X] = x
  • condizionali
  • controllo dell'uguaglianza
    • il valore di livello: a:A == b:B
    • tipo di livello: implicitly[A =:= B]
    • a livello di valore: si verifica nella JVM tramite uno unit test in fase di esecuzione (ovvero senza errori di runtime):
      • in essense è un'affermazione: assert(a == b)
    • livello di tipo: si verifica nel compilatore tramite un controllo del tipo (cioè nessun errore del compilatore):
      • in sostanza è un confronto di tipo: es implicitly[A =:= B]
      • A <:< B, viene compilato solo se Aè un sottotipo diB
      • A =:= B, viene compilato solo se Aè un sottotipo di Bed Bè un sottotipo diA
      • A <%< B, ("visualizzabile come") viene compilato solo se Aè visualizzabile come B(cioè c'è una conversione implicita da Aa un sottotipo di B)
      • un esempio
      • più operatori di confronto

Conversione tra tipi e valori

  • In molti degli esempi, i tipi definiti tramite i tratti sono spesso sia astratti che sigillati, e quindi non possono essere istanziati direttamente né tramite sottoclasse anonime. Quindi è comune utilizzare nullcome valore segnaposto quando si esegue un calcolo a livello di valore utilizzando un tipo di interesse:

    • ad esempio val x:A = null, dov'è Ail tipo a cui tieni
  • A causa della cancellazione del tipo, i tipi parametrizzati hanno tutti lo stesso aspetto. Inoltre, (come accennato in precedenza) i valori con cui stai lavorando tendono a essere tutti null, e quindi il condizionamento sul tipo di oggetto (ad esempio tramite una dichiarazione di corrispondenza) è inefficace.

Il trucco sta nell'usare funzioni e valori impliciti. Il caso base è solitamente un valore implicito e il caso ricorsivo è solitamente una funzione implicita. In effetti, la programmazione a livello di tipo fa un uso massiccio di impliciti.

Considera questo esempio ( tratto da metascala e apocalisp ):

sealed trait Nat
sealed trait _0 extends Nat
sealed trait Succ[N <: Nat] extends Nat

Qui hai una codifica peano dei numeri naturali. Cioè, hai un tipo per ogni numero intero non negativo: un tipo speciale per 0, vale a dire _0; e ogni numero intero maggiore di zero ha un tipo della forma Succ[A], dove Aè il tipo che rappresenta un numero intero più piccolo. Ad esempio, il tipo che rappresenta 2 sarebbe: Succ[Succ[_0]](successore applicato due volte al tipo che rappresenta zero).

Possiamo alias vari numeri naturali per un riferimento più conveniente. Esempio:

type _3 = Succ[Succ[Succ[_0]]]

(Questo è un po 'come definire vala come risultato di una funzione.)

Ora, supponiamo di voler definire una funzione a livello di valore def toInt[T <: Nat](v : T)che accetta un valore di argomento v,, che è conforme a Nate restituisce un numero intero che rappresenta il numero naturale codificato nel vtipo di. Ad esempio, se abbiamo il valore val x:_3 = null( nulldi tipo Succ[Succ[Succ[_0]]]), lo vorremmotoInt(x) tornare 3.

Per l'implementazione toInt, utilizzeremo la seguente classe:

class TypeToValue[T, VT](value : VT) { def getValue() = value }

Come vedremo di seguito, ci sarà un oggetto costruito dalla classe TypeToValueper ciascuno Natdal _0fino al (es.) _3, E ognuno memorizzerà la rappresentazione del valore del tipo corrispondente (cioè TypeToValue[_0, Int]memorizzerà il valore 0, TypeToValue[Succ[_0], Int]memorizzerà il valore 1, ecc.). Nota, TypeToValueè parametrizzato da due tipi: Te VT. Tcorrisponde al tipo a cui stiamo cercando di assegnare valori (nel nostro esempio, Nat) e VTcorrisponde al tipo di valore che gli stiamo assegnando (nel nostro esempio,Int ).

Ora facciamo le seguenti due definizioni implicite:

implicit val _0ToInt = new TypeToValue[_0, Int](0)
implicit def succToInt[P <: Nat](implicit v : TypeToValue[P, Int]) = 
     new TypeToValue[Succ[P], Int](1 + v.getValue())

E implementiamo toIntcome segue:

def toInt[T <: Nat](v : T)(implicit ttv : TypeToValue[T, Int]) : Int = ttv.getValue()

Per capire come toIntfunziona, consideriamo cosa fa su un paio di input:

val z:_0 = null
val y:Succ[_0] = null

Quando chiamiamo toInt(z), il compilatore cerca un argomento implicito ttvdi tipo TypeToValue[_0, Int](poiché zè di tipo _0). Trova l'oggetto _0ToInt, chiama il getValuemetodo di questo oggetto e torna indietro0 . Il punto importante da notare è che non abbiamo specificato al programma quale oggetto utilizzare, il compilatore lo ha trovato implicitamente.

Ora consideriamo toInt(y). Questa volta, il compilatore cerca un argomento implicito ttvdi tipo TypeToValue[Succ[_0], Int](poiché yè di tipo Succ[_0]). Trova la funzione succToInt, che può restituire un oggetto del tipo appropriato ( TypeToValue[Succ[_0], Int]) e la valuta. Questa funzione stessa accetta un argomento implicito ( v) di tipo TypeToValue[_0, Int](ovvero, a TypeToValuedove il primo parametro di tipo è ne ha uno in meno Succ[_]). Il compilatore fornisce _0ToInt(come è stato fatto nella valutazione di cui toInt(z)sopra) e succToIntcostruisce un nuovo TypeToValueoggetto con valore1 . Ancora una volta, è importante notare che il compilatore fornisce tutti questi valori in modo implicito, poiché non abbiamo accesso ad essi esplicitamente.

Controllo del tuo lavoro

Esistono diversi modi per verificare che i calcoli a livello di tipo stiano facendo ciò che ti aspetti. Ecco alcuni approcci. Crea due tipi Ae Bche vuoi verificare siano uguali. Quindi controlla che la seguente compilazione:

In alternativa, puoi convertire il tipo in un valore (come mostrato sopra) ed eseguire un controllo di runtime dei valori. Ad esempio assert(toInt(a) == toInt(b)), dove aè di tipo Aed bè di tipo B.

Risorse addizionali

L'insieme completo dei costrutti disponibili può essere trovato nella sezione tipi del manuale di riferimento scala (pdf) .

Adriaan Moors ha diversi documenti accademici sui costruttori di tipi e argomenti correlati con esempi da scala:

Apocalisp è un blog con molti esempi di programmazione a livello di tipo in scala.

ScalaZ è un progetto molto attivo che fornisce funzionalità che estendono l'API Scala utilizzando varie funzionalità di programmazione a livello di tipo. È un progetto molto interessante che ha un grande seguito.

MetaScala è una libreria a livello di tipo per Scala, inclusi meta tipi per numeri naturali, booleani, unità, HList, ecc. È un progetto di Jesper Nordenberg (il suo blog) .

Il Michid (blog) ha alcuni fantastici esempi di programmazione a livello di tipo in Scala (da un'altra risposta):

Debasish Ghosh (blog) ha anche alcuni post rilevanti:

(Ho fatto delle ricerche su questo argomento ed ecco cosa ho imparato. Sono ancora nuovo, quindi per favore indica eventuali inesattezze in questa risposta.)


12

Volevo solo dire grazie per l'interessante blog; Lo seguo da un po 'e soprattutto l'ultimo post menzionato sopra ha affinato la mia comprensione delle proprietà importanti che un sistema di tipi per un linguaggio orientato agli oggetti dovrebbe avere. Quindi grazie!
Zach Snow



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.