Miscelazione del codice dichiarativo e imperativo ('unit test' impliciti?) [chiuso]

1

Beh, non sono un esperto, ma come studente sono curioso delle lingue e dei loro modelli / obiettivi di design.

Mi piacerebbe sapere se ci sono dei punti che mi mancano nei seguenti esempi e perché tecniche come questa non sono ampiamente usate nelle lingue popolari. Perché è un'idea migliore in un programma di vita reale definire esplicitamente tutti i test unitari, invece di usare un codice dichiarativo. perché le lingue non usano alcune dichiarazioni come test e implementazione in generale? Perché dividere queste cose in due parti?

Supponiamo di avere i seguenti test unitari:

assertRaises(avg([]), ValueError)
assertEquals(avg([1], 1)
assertEquals(avg([1, -2]), -.5)
assertEquals(avg([1, 2]), .5)

Se prendi questo pseudocodice,

def avg(items):
    if len(items) == 0:
        raise ValueError
    else if len(items) == 1:
        return items[0]
    else:
        return sum(items)/len(items)

la maggior parte delle parti di avg sembra non fare nulla, o molto poco più che dichiarare solo il modo di superare alcuni test unitari. In altre parole, per me sembra un po 'come la duplicazione del codice. Soprattutto il primo test sembra essere superfluo in questo esempio.

Mi chiedo se sarebbe un buon progetto sbarazzarsi di alcuni test unitari in questo modo:

def avg(items) {
    len(items) == 0 => raises ValueError
    len(items) == 1 => items[0]
    len(items) > 1 => sum(items)/len(items)
    (test) items == [1, -2] => $avg == -.5
    (test) items == [1, 2] => $avg == .5
    (test) avg(items) <= sum(items)
}

In questo esempio, (test) indica quando uno specifico caso di test non è una definizione utile (anche se forse questi potrebbero essere utilizzati anche per l'ottimizzazione in rari casi). Ovviamente, questo pseudocodice non è ben progettato perché è meno leggibile rispetto alla prima implementazione, ma penso che sia un modo più conveniente per dichiarare i test unitari. In realtà i test unitari sembrano essere sbagliati qui, dal momento che non fanno altro che testare la definizione. E l'ultimo test non è banale da eseguire. Ma può essere eseguito ad esempio, quando viene eseguito un altro test, che verifica un codice che contiene avg in esso, quindi è probabile che fornisca un utile set di parametri di test. Inoltre, ad esempio, anche l'ultimo test unitario ha un ruolo secondario! Se ho un codice come questo:

if avg(a) <= sum(a)

Non è affatto necessario valutare la definizione complessa, poiché in fase di esecuzione uno dei casi di test lo implica direttamente. OK, in questo caso, non è una vera ottimizzazione (anche se un IDE potrebbe avvisare l'utente, che l'espressione sarà sempre vera, che è utile). Tuttavia, posso immaginare, che ci sono molti esempi complessi, che possono essere ottimizzati in questo modo, senza che lo sviluppatore verifichi esplicitamente casi speciali in ogni singolo luogo in cui sono rilevanti.

Forse un esempio migliore per questa ottimizzazione

def power(a, n) {
    n == 0 => 1
    n == 1 => a
    n > 1 => a * power(a, n - 1)
    (test) for any (x) power(a, n) % x == power(a % x, n) % x
}

Non è un segreto, penso, che un compilatore intelligente potrebbe usarlo in molti casi, e un interprete intelligente / compilatore just-in-time potrebbe usarli in molti più casi.

Per comprendere meglio test e linguaggi, sono curioso di sapere se i miei pensieri sono utili, e se no, cosa mi manca, o se sì, perché le lingue popolari non implementano tali schemi.

    
posta kdani 18.02.2014 - 21:47
fonte

1 risposta

3

Il "compilatore sufficientemente avanzato" è diventato uno scherzo comune quando si parla di linguaggi di programmazione. Alcuni compilatori hanno in realtà caratteristiche sorprendenti, ma spesso questo è usato come scusa per la progettazione di un linguaggio sciatto o per l'esecuzione di alcuni linguaggi dinamici che non hanno un compilatore così avanzato.

Il modo in cui hai definito le tue funzioni mi ricorda la corrispondenza del modello , una funzionalità che si trova più spesso in linguaggi funzionali simili a ML, ad es. lungo le linee di

let avg xs =
  match xs with
  | []      => throw (ValueError "array must contain at least one element")
  | x :: [] => x
  | xs      => (fold (+) xs) / (array_length xs)

Questo è interessante perché possiamo specificare la semantica di alcune lingue come un insieme di regole di riscrittura. Tale definizione di funzione specifica i modi per riscrivere un'applicazione del modulo avg [1, 2, 3] . Certamente questa frase dichiarativa della soluzione rende più facile applicare le ottimizzazioni. Ma sarebbe molto più difficile, data la forma più simile a C?

Num avg(Num[] xs) {
    if (xs.length == 0) throw new ValueError();
    if (xs.length == 1) return xs[0];
    return sum(xs)/xs.length;
}

Un compilatore che sia a conoscenza degli idiomi della lingua non troverà più difficile ragionare su tale rappresentazione.

Ora alla tua domanda principale, sui test unitari. Il valore dei test è questa ripetizione . Idealmente, i test e l'implementazione sono scritti da due persone diverse, il che evita che un bug venga copiato e incollato da una rappresentazione all'altra. In altre parole: quali sono le possibilità che due persone non riescono a capire il problema o hanno commesso lo stesso errore di battitura? (Ad esempio, avg([-1, -3]) <= sum([-1, -3]) non è corretto).

Il test asserisce che una certa interfaccia sarà onorato e controlla gli input rappresentativi per darci una buona sicurezza che l'implementazione funzioni come previsto. Per semplici pezzi di codice come indicato qui, potrebbe essere ovviamente corretto senza dover scrivere molti test. Tuttavia, potremmo voler cambiare il funzionamento interno della nostra implementazione. Vogliamo assicurarci che il nostro codice funzioni ancora come previsto dopo ogni modifica, anche se un'implementazione folle che utilizza la riflessione o altre cose non ottimizzabili e potrebbe non essere ovviamente corretta . Pertanto, un'implementazione non può essere il suo test allo stesso tempo e noi abbiamo per codificare l'interfaccia in un altro, secondo modo. Una volta fatto ciò, è davvero facile mantenere la compatibilità all'indietro - i tuoi utenti ti ringrazieranno

Negli esempi che hai fornito, un test può essere scritto su una singola riga. In altri casi, questo non è assolutamente il caso. Oggi ho aperto un file di test con 100 righe per impostare una semplice grammatica, tutto solo per un singolo test che ha verificato che l'implementazione di un lexer potesse interfacciarsi correttamente con questa grammatica. Inserire un test così prolisso all'interno dell'implementazione renderebbe il codice effettivo eccessivamente difficile da leggere a causa di tutte le interruzioni. Posizionare i test in un file separato elimina ogni problema di leggibilità e semplifica il mio strumento di test di scelta per " prove "Correttezza della mia implementazione.

    
risposta data 18.02.2014 - 22:39
fonte