Spesso ho bisogno di aggiungere campi a un ADT che memorizza solo alcune informazioni ridondanti. Ma non ho capito completamente come farlo in modo piacevole ed efficiente.Come aggiungere campi che memorizzano solo qualcosa in ADT?
Il modo migliore per mostrare il problema è fare un esempio. Supponiamo che stiamo lavorando con i termini lambda senza tipo:
type VSym = String
data Lambda = Var VSym
| App Lambda Lambda
| Abs VSym Lambda
E di tanto in tanto abbiamo bisogno di calcolare l'insieme di variabili libere di un termine:
fv :: Lambda -> Set VSym
fv (Var v) = Set.singleton v
fv (App s t) = (fv s) `Set.union` (fv t)
fv (Abs v t) = v `Set.delete` (fv t)
Presto ci rendiamo conto che ripetuti calcoli di fv
sono un collo di bottiglia della nostra applicazione. Vorremmo aggiungerlo in qualche modo al tipo di dati. Come:
data Lambda1 = Var (Set VSym) VSym
| App (Set VSym) Lambda Lambda
| Abs (Set VSym) VSym Lambda
Ma rende la definizione abbastanza brutta. Quasi lo (Set VSym)
occupa più spazio di tutto il resto. Inoltre, interrompe la corrispondenza del modello in tutte le funzioni che utilizzano Lambda
. E per peggiorare le cose, se in seguito decidiamo di aggiungere qualche altro campo di memoizzazione, dovremo riscrivere di nuovo tutti i modelli.
Come progettare una soluzione generale che consente di aggiungere tali campi di memoizzazione in modo semplice e discreto? Mi piacerebbe raggiungere i seguenti obiettivi:
- La definizione
data
dovrebbe cercare il più vicino possibile a quello originale, in modo che sia facilmente leggibile e comprensibile. - Anche le corrispondenze di pattern devono rimanere semplici e leggibili.
- L'aggiunta di un nuovo campo memoizing entro e non deve rompere il codice esistente, in particolare:
- di non rompere gli schemi esistenti,
- di non richiedere modifiche in cui è stata utilizzata la funzione che vogliamo Memoize (come il codice che ha utilizzato
fv
in questo esempio).
descriverò la mia soluzione attuale: Per mantenere la definizione data
e delle corrispondenze come poco ingombrante possibile, definiamo:
data Lambda' memo = Var memo VSym
| App memo (Lambda' memo) (Lambda' memo)
| Abs memo VSym (Lambda' memo)
type Lambda = Lambda' LambdaMemo
in cui i dati da memoized è definito separatamente:
data LambdaMemo = LambdaMemo { _fv :: Set VSym, _depth :: Int }
Quindi come funzione di esecuzione che recupera la parte memoized:
memo :: Lambda' memo -> memo
memo (Var c _) = c
memo (App c _ _) = c
memo (Abs c _ _) = c
(Questo potrebbe essere eliminato utilizzando campi denominati. Ma poi avremmo anche have to name all the other fields.)
Questo ci permette di raccogliere parti specifiche dal Memoize, mantenendo la stessa firma di fv
come prima:
fv :: Lambda -> Set VSym
fv = _fv . memo
depth :: Lambda -> Int
depth = _depth . memo
Infine, dichiariamo questi costruttori intelligenti:
var :: VSym -> Lambda
var v = Var (LambdaMemo (Set.singleton v) 0) v
app :: Lambda -> Lambda -> Lambda
app s t = App (LambdaMemo (fv s `Set.union` fv t) (max (depth t) (depth s))) s t
abs :: VSym -> Lambda -> Lambda
abs v t = Abs (LambdaMemo (v `Set.delete` fv t) (1 + depth t)) v t
Ora possiamo scrivere in modo efficiente le cose che combinano la corrispondenza del modello con la lettura dei campi memoizzati come
canSubstitute :: VSym -> Lambda -> Lambda -> Bool
canSubstitute x s t
| not (x `Set.member` (fv t))
= True -- the variable doesn't occur in `t` at all
canSubstitute x s [email protected](Abs _ u t')
| u `Set.member` (fv s)
= False
| otherwise
= canSubstitute x s t'
canSubstitute x s (Var _ _)
= True
canSubstitute x s (App _ t1 t2)
= canSubstitute x s t1 && canSubstitute x s t2
Questo sembra risolvere:
- La corrispondenza del modello è ancora abbastanza ragionevole.
- Se aggiungiamo un nuovo campo di memoizzazione non interromperà il codice esistente.
- Se decidiamo di memoizzare una funzione con la firma
Lambda -> Something
, possiamo facilmente aggiungerla come nuovo campo di memoizzazione.
Quello che ancora non mi piace di questo progetto:
- La definizione
data
non è poi così male, ma ancora mettendomemo
ovunque ingombra notevolmente. - Abbiamo bisogno di costruttori intelligenti per la costruzione di valori, ma usiamo i costruttori regolari per la corrispondenza dei modelli. Non è così male, semplicemente aggiungiamo uno
_
, ma avere la stessa firma per la costruzione e la decostruzione sarebbe bello. Suppongo che Views o Pattern Synonyms risolverà. - Il calcolo dei campi memoizzati (variabili libere, profondità) è strettamente accoppiato ai costruttori intelligenti. Poiché è ragionevole presumere che quelle funzioni memoizzate saranno sempre catamorphisms, credo che questo potrebbe essere risolto in una certa misura da strumenti come the fixpoint package.
Qualche idea su come migliorarlo? O ci sono modi migliori per risolvere questo problema?
Forse il Annotations' pacchetto '(ci potrebbero essere altre biblioteche che offrono funzionalità simili) vi aiutano? – kosmikus
@kosmikus Ho esaminato brevemente il pacchetto e temo che la corrispondenza del modello diventi scomoda se viene utilizzata tale libreria. Forse, se esprimessimo tutte le funzioni che funzionano con 'Lambda' come ana/morfismo di cata (che non mi dispiacerebbe affatto), i modelli in esse potrebbero diventare ragionevoli. Forse varrebbe la pena di rendere il tuo commento una risposta completa? –