Ho programmato per anni con linguaggi prevalentemente imperativi (C ++, C #, javascript, python), ma recentemente ho sperimentato alcuni linguaggi funzionali (Lisp, Haskell) ed ero entusiasta di provare ad applicare alcuni degli stili dichiarativi idee di programmazione in C ++. Ho una libreria di sostituzione STL basata su intervallo personalizzata che ho scritto un po 'indietro che ha reso molto possibile ciò in modo abbastanza pulito.
Ecco un esempio: una funzione per verificare se esiste una sottostringa di destinazione all'interno di una stringa di origine, ignorando il caso. In primo luogo il modo imperativo semplice:
bool StringContains(const string& source, const string& target) {
// Figure out search area, exit if target is too big to exist in source
if (target.size() > source.size()) {
return false;
}
size_t endIndex = source.size() - target.size();
// For each potential position...
for (size_t i = 0; i <= endIndex; i++) {
// Check if target is here
size_t strPos = i;
bool foundHere = true;
for (char targetChar : target) {
char strChar = tolower(source[strPos]);
targetChar = tolower(targetChar);
if (strChar != targetChar) {
foundHere = false;
break;
}
strPos++;
}
// If found here, return true
if (foundHere) {
return true;
}
}
// If not found by now, return false
return false;
}
E qui sta usando la mia libreria dichiarativa (che usa un po 'di magia C ++ 11):
bool StringContainsDec(const string& source, const string& target) {
// Figure out search area, exit if target is too big to exist in source
if (target.size() > source.size()) {
return false;
}
size_t endIndex = source.size() - target.size();
// For each potential position...
auto targetRange = All(target) | Transformed(tolower);
for (size_t i = 0; i <= endIndex; i++) {
// If found here, return true
auto sourceRange = All(source) | Sliced(i, i + target.size()) |
Transformed(tolower);
if (RangesMatch(sourceRange, targetRange)) {
return true;
}
}
// If not found by now, return false
return false;
}
Un po 'più compatto e forse inglese e leggibile, il che è bello. Il "|" è analogo a una pipe di script di shell, i valori di routing passano all'operazione successiva. Quindi:
All(source) | Sliced(i, i + target.size()) | Transformed(tolower)
significa, imposta un intervallo che, una volta iterato, prenderà ciascun carattere di 'source', suddiviso tra index i e i + target.size (), e passerà ciascun carattere attraverso tolower ().
RangesMatch () itera ciascuno dei due intervalli e restituisce true se ogni elemento corrisponde.
Quindi, va tutto bene, e funziona correttamente. Ma nel tempo ho trovato, sperimentando questo approccio in situazioni pratiche:
- Il codice dichiarativo è più difficile da eseguire il debug. Con l'imperativo, puoi semplicemente entrare nel debugger, riga per riga, e vedere cosa sta succedendo. Con il dichiarativo, non è molto più complesso, ma è necessario passare attraverso alcune diverse funzioni di libreria per costruire l'intervallo, chiamando le funzioni di iteratore interno (Front (), PopFront (), ecc.). Così ti salta in giro da un posto all'altro, rendendolo più confuso per tracciare la logica. Immagino che questo sia più facile ad es. un debugger Lisp.
- Il codice dichiarativo è un po 'più lento. Sul mio sistema è circa la metà della velocità del codice imperativo. Le gamme sono costruite pigramente e molto efficienti, e allocano solo i locali in pila, ma coinvolgono ancora un po 'di più, come il tracciamento dei puntatori inizio / fine, che si sommano nei loop nidificati ecc. Con dichiarativo sembra possibile facilmente perdere il contatto con ciò che il tuo codice sta effettivamente facendo. Se hai una grande catena di operazioni perderai le opportunità per semplificare, salva i valori intermedi utili in modo che non debbano essere ricalcolati in seguito, ecc.
- Il codice dichiarativo è più difficile da modificare nel tempo, trovo. Se voglio fare qualche operazione in più su ogni personaggio, ho bisogno di aggiungere un'altra funzione di trasformazione, o lambda ecc. Nella programmazione imperativa aggiungo semplicemente una riga di codice ol normale all'interno del ciclo, o 100 righe se necessario, ed è abbastanza facile da seguire.
- Trovo che lo stile imperativo sia più intuitivo mentre sto scrivendo. Rispecchia meglio l'ordine in cui accadono le cose, procediamo passo dopo passo senza dover destreggiarmi in testa, ecc.
Ora tutta questa roba potrebbe essere particolare per la mia implementazione o le mie preferenze, ma immagino che alcune di esse siano inerenti anche allo stile? Questa funzione di stringa è solo un esempio, ma l'ho trovata con tutti i tipi di cose quando ho implementato entrambe le cose fianco a fianco - che l'80% del tempo di stile imperativo vince per me, basta farlo con semplici vecchi cicli e se le affermazioni piuttosto che scherzando con le funzioni di ordine superiore, mappa / riduci, ecc. Potrebbero aggiungere una certa brevità del codice e un po 'meno di battitura se il tuo editor di testo fa schifo, ma in situazioni complicate del mondo reale diventano confuse e difficili da mantenere.
Quindi è dichiarativo sopravvalutato? Qualcuno ha avuto una vasta esperienza con entrambi gli approcci, specialmente con progetti complessi nel mondo reale in linguaggi funzionali? Curioso di sentire cosa pensano gli altri.