La programmazione dichiarativa è sopravvalutata? [chiuso]

3

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.

    
posta QuadrupleA 09.01.2016 - 08:15
fonte

2 risposte

13

The declarative code is harder to debug.

Direi che è una funzione della qualità del tuo debugger. Se il debugger comprende i costrutti imperativi ma non quelli dichiarativi, allora naturalmente quelli delarativi sono più difficili da eseguire. Ma potresti facilmente immaginare un debugger diverso con priorità diverse, dove è vero il contrario.

sono alcuni progettisti di linguaggi che fanno si preoccupano così tanto degli strumenti che sono persino disposti a lasciare che la toolability influenzi la progettazione linguistica o addirittura comprometta le funzionalità linguistiche per facilitare utensili. L'esempio ovvio è Kotlin, che è stato progettato da un fornitore di strumenti (JetBrains). Gli sviluppatori principali di Scala sono anche contrari all'espansione dell'inferenza di tipo di Scala, non perché non sanno come farlo (lo fanno) o perché è difficile da implementare (lo è, ma hanno scrittori di compilatori intelligenti), ma perché non hanno trovato un modo per implementarlo con buoni messaggi di errore . (Pensa agli errori di istanziazione del modello C ++ a metà degli anni '90.)

The declarative code is a bit slower. […] With declarative it seems like you can easily lose touch with what your code is actually doing.

Sì. Questo è il punto intero . Ecco perché è chiamato "dichiarativo": perché dichiari che vuoi che succeda, non come vuoi che accada.

Questo dà un margine di lotto in più per il compilatore per ottimizzare le cose.

C'è un grande esempio in uno dei supercomputer Supercomputer per Haskell. L'autore confronta una funzione di conteggio di parole semplice, espressiva, dichiarativa, puramente funzionale di una riga in Haskell ( main = print . length . words =<< getContents ), compilata con una combinazione di Supero, GHC e YHC con una percentuale di co_de basata sullo stato ottimizzato a mano % loop in C, e molto alla sua sorpresa trova che Haskell è leggermente più veloce. Come potrebbe accadere? Bene, il compilatore ha effettivamente trasformato il codice Haskell nello stesso loop della macchina di stato che ha la versione C scritta a mano, ma può fare un trucco aggiuntivo che C (almeno senza assembly in linea) non può: codificare lo stato (s) ) nel contatore del programma.

Nel tuo caso, hai creato un DSL dichiarativo, se lo desideri. Ma l'ottimizzatore C ++ non sa nulla della semantica del tuo DSL, quindi non può trarre vantaggio dalla maggiore libertà.

The declarative code is harder to modify over time, I find. If I want to do some extra operation on each character, I need to add another […] lambda etc. In imperative programming I just add a line of plain ol' code inside the loop […].

Non seguo. C'è davvero differenza tra:

step1();
step2();
step2a(); // inserted later
step3();

e

transform1    | 
  transform2  |
  transform2a | // inserted later
  transform3;

I find the imperative style more intuitive as I'm writing.

Non esiste una cosa (assoluta) come "intuitiva". L'intuito è tutto sulla familiarità. Ricorda il film Star Trek, quando Scotty tenta di utilizzare un computer con ciò che noi consideriamo un'interfaccia utente intuitiva? Finisce per provare a pronunciare comandi vocali nel mouse.

Molte persone considerano i loop intuitivi e la ricorsione non intuitiva. Tuttavia, solo un paio di mesi fa, c'era una domanda nel tag Ruby su StackOverflow da un principiante di programmazione completo, che aveva scritto codice come questo:

def main
  # do something
  main
end

Per lui, questo era il modo intuitivo di fare qualcosa più e più volte. (E perché no? "Fai qualcosa, e poi ricomincia quello che stai facendo" è un modello mentale perfettamente sensato per quello che noi ragazzi imperativi chiamiamo un "loop", non è vero?) E per un programmatore Scheme, ML o Haskell , questo sarebbe intuitivo, e loop non lo farebbe. (In effetti, un programmatore ML o Haskell puro non saprebbe nemmeno di cosa stiamo parlando, perché i loro linguaggi non hanno loop .)

Un altro esempio da me personalmente: come programmatore di Ruby e fan di Smalltalk, non riesco a capire perché qualcuno vorrebbe mai un compilatore AOT statico. Eppure, la comunità C ++ non riesce a capire perché qualcuno vorrebbe mai un compilatore JIT dinamico.

A meno che e fino a quando non avrai scritto la stessa quantità di codice (serio, non giocattolo, complesso, grande, di livello di produzione) in entrambi gli stili, lo stile che è più familiare sarà più "intuitivo". Questa è solo la natura delle cose.

    
risposta data 09.01.2016 - 09:20
fonte
3

La programmazione dichiarativa IMHO è molto ambigua.

In particolare, ha avuto un significato diverso in diversi paesi (in Francia non è la stessa degli Stati Uniti) e in tempi diversi (prima o dopo Inverni AI ).

La mia comprensione di ciò è quella di Jacques Pitrat che parla in modo più saggio di dichiarativo conoscenza :

we give knowledge in a declarative form, which does not include how to use it.

In un certo modo, la programmazione dichiarativa è una contraddizione in termini; la programmazione può essere vista come configurazione dei computer su come fare le cose, ma dichiarativa significa che il sistema dovrebbe trovarsi su come fare le cose, e dichiariamo solo ciò che vogliamo che faccia, non come fare esso.

Una volta che abbiamo un sistema che comprende la conoscenza dichiarativa, la "programmazione" dovrebbe essere abbastanza semplice: diamo separatamente molte conoscenze dichiarative (inclusa la conoscenza dichiarativa su come compilare e usare la conoscenza dichiarativa) e alcuni obiettivi e obiettivi. Questo è anche il sogno di alcuni AGI sistema, e J.Pitrat ha scritto molto su tesi.

E la programmazione potrebbe essere estesa alla nozione di scrivere codice sorgente: lo sviluppatore scrive alcune formalizzazioni comprese da qualche sistema. Questo codice sorgente è la formalizzazione preferita per lo sviluppatore (questa è la definizione di codice sorgente per gli appassionati di software libero).

In realtà, c'è uno spettro continuo tra conoscenza dichiarativa e procedurale ....

Quindi la programmazione funzionale IMHO non è esattamente una programmazione dichiarativa, ma in effetti i linguaggi funzionali sono più dichiarativi di quelli procedurali. Inoltre, i sistemi dichiarativi non sono sopravvalutati, ma hai bisogno di alcune decine di anni per svilupparli (leggi il mitico mese uomo )

Per una visione pratica a breve termine, la programmazione dichiarativa può semplicemente significare favorire i dati (compresi i dati di configurazione "dichiarativi" che forniscono alcuni "obiettivi") sul codice.

Leggi anche su sistemi esperti .

PS. Consiglio vivamente di leggere blog di J.Pitrat e libri. Ha dedicato la sua intera vita a conoscenza dichiarativa .

    
risposta data 09.01.2016 - 09:17
fonte