Se tutto ciò che volevi fosse un linguaggio tipizzato staticamente che assomigliasse al Lisp, potresti farlo piuttosto facilmente, definendo un albero di sintassi astratto che rappresenta la tua lingua e quindi mappando quell'AST su S-espressioni. Tuttavia, non credo che chiamerei il risultato Lisp.
Se vuoi qualcosa che abbia effettivamente le caratteristiche Lisp-y oltre alla sintassi, è possibile farlo con un linguaggio tipizzato staticamente. Tuttavia, ci sono molte caratteristiche del Lisp dalle quali è difficile ottenere una tipizzazione statica molto utile. Per illustrare, diamo uno sguardo alla struttura dell'elenco stessa, chiamata contro , che costituisce l'elemento costitutivo principale di Lisp.
Chiamare i contro una lista, anche se (1 2 3)
sembra uno, è un po 'improprio. Ad esempio, non è affatto paragonabile a un elenco digitato staticamente, come l'elenco di C ++ std::list
o di Haskell. Questi sono elenchi collegati unidimensionali in cui tutte le celle sono dello stesso tipo. Lisp lo consente felicemente (1 "abc" #\d 'foo)
. Inoltre, anche se estendi gli elenchi di tipo statico per coprire elenchi di elenchi, il tipo di questi oggetti richiede che ogni elemento dell'elenco sia un sottolista. Come rappresenteresti ((1 2) 3 4)
in loro?
Lisp conses forma un albero binario, con foglie (atomi) e rami (conses). Inoltre, le foglie di un albero del genere possono contenere qualsiasi tipo Lisp atomico (non contro)! La flessibilità di questa struttura è ciò che rende Lisp così bravo a gestire calcoli simbolici, AST e trasformare il codice Lisp stesso!
Quindi come modellereste una struttura del genere in un linguaggio tipizzato staticamente? Proviamolo in Haskell, che ha un sistema di tipo statico estremamente potente e preciso:
type Symbol = String
data Atom = ASymbol Symbol | AInt Int | AString String | Nil
data Cons = CCons Cons Cons
| CAtom Atom
Il tuo primo problema sarà l'ambito del tipo Atom. Chiaramente, non abbiamo scelto un tipo Atom di flessibilità sufficiente per coprire tutti i tipi di oggetti che vogliamo lanciare in giro. Invece di cercare di estendere la struttura dei dati Atom come elencato sopra (che puoi vedere chiaramente è fragile), diciamo che abbiamo avuto una classe di tipi magici Atomic
che distingueva tutti i tipi che volevamo rendere atomici. Quindi potremmo provare:
class Atomic a where ?????
data Atomic a => Cons a = CCons Cons Cons
| CAtom a
Ma questo non funzionerà perché richiede che tutti gli atomi dell'albero siano dello stesso tipo. Vogliamo che siano in grado di differire da foglia a foglia. Un approccio migliore richiede l'uso dei quantificatori esistenziali di Haskell :
class Atomic a where ?????
data Cons = CCons Cons Cons
| forall a. Atomic a => CAtom a
Ma ora arrivi al nocciolo della questione. Cosa puoi fare con gli atomi in questo tipo di struttura? Quale struttura hanno in comune con cui si potrebbe modellare Atomic a
? Quale livello di sicurezza del tipo ti viene garantito con un tale tipo? Nota che non abbiamo aggiunto alcuna funzione alla nostra classe di tipo, e c'è una buona ragione: gli atomi non condividono nulla in comune in Lisp. Il loro supertipo in Lisp è semplicemente chiamato t
(cioè top).
Per usarli, dovresti inventare meccanismi per forzare dinamicamente il valore di un atomo a qualcosa che puoi effettivamente usare. E a quel punto, hai praticamente implementato un sottosistema tipizzato dinamicamente all'interno del tuo linguaggio tipizzato staticamente! (Non si può fare a meno di notare un possibile corollario alla Decima regola di programmazione di Greenspun .)
Si noti che Haskell fornisce il supporto solo per un sottosistema dinamico di questo Obj
tipo con un tipo, utilizzato insieme a un Dynamic
tipo e una classe Typeable per sostituire la nostra Atomic
classe, che consentono di memorizzare valori arbitrari con i loro tipi e la coercizione esplicita da quei tipi. Questo è il tipo di sistema che dovresti usare per lavorare con le strutture contro Lisp nella loro piena generalità.
Quello che puoi fare è anche andare dall'altra parte e incorporare un sottosistema tipizzato staticamente all'interno di un linguaggio tipizzato essenzialmente dinamicamente. Ciò consente il vantaggio del controllo del tipo statico per le parti del programma che possono trarre vantaggio da requisiti di tipo più rigorosi. Questo sembra essere l'approccio adottato nella forma limitata di controllo del tipo preciso della CMUCL , ad esempio.
Infine, c'è la possibilità di avere due sottosistemi separati, tipizzati dinamicamente e staticamente, che utilizzano la programmazione in stile contratto per aiutare a navigare nella transizione tra i due. In questo modo il linguaggio può adattarsi agli usi del Lisp in cui il controllo del tipo statico sarebbe più un ostacolo che un aiuto, così come gli usi in cui il controllo del tipo statico sarebbe vantaggioso. Questo è l'approccio adottato da Typed Racket , come vedrai dai commenti che seguono.