Come avete descritto, praticamente qualsiasi controllo che può essere eseguita in ->i
può essere eseguita all'interno della funzione stessa, ma poi di nuovo, qualsiasi controllo eseguito dai contratti può, per la maggior parte, essere eseguita entro le funzioni stesse . La codifica delle informazioni in un contratto offre alcuni vantaggi.
- È possibile estrarre gli invarianti dall'implementazione della funzione. Questo è bello perché non hai bisogno di ingombrare la funzione stessa con clausole di guardia, puoi semplicemente scrivere il codice e conoscere che gli invarianti saranno mantenuti dal contratto.
- I contratti sono difficili da fornire una buona segnalazione degli errori. Assegneranno automaticamente "colpa" alla parte che viola il contratto e, per contratti complessi, aggiungeranno il contesto ai messaggi di errore per rendere il più chiaro possibile il problema.
Questi sono più evidenti con ->i
quando il contratto deve specificare dipendenze all'interno argomenti forniti alla funzione. Ad esempio, dispongo di una raccolta di raccolte, che include una funzione subsequence
. Richiede tre argomenti, una sequenza, un indice iniziale e un indice finale. Questo è il contratto che uso per proteggerlo:
(->i ([seq sequence?]
[start exact-nonnegative-integer?]
[end (start) (and/c exact-nonnegative-integer? (>=/c start))])
[result sequence?])
Questo mi permette di specificare in modo esplicito che l'indice finale deve essere maggiore o uguale a l'indice di partenza, e non devono preoccuparsi di verificare che invariante nella mia funzione. Quando ho violato il presente contratto, ottengo un bel messaggio di errore:
> (subsequence '() 2 1)
subsequence: contract violation
expected: (and/c exact-nonnegative-integer? (>=/c 2))
given: 1
which isn't: (>=/c 2)
Può essere utilizzato per garantire invarianti più complessi, anche. Definisco anche la mia funzione map
, che, come il built-in map
di Racket, supporta numeri variabili di argomenti. La procedura fornita a map
deve accettare lo stesso numero di argomenti in base alla sequenza fornita. Io uso il seguente contratto per map
:
(->i ([proc (seqs) (and/c (procedure-arity-includes/c (length seqs))
(unconstrained-domain-> any/c))])
#:rest [seqs (non-empty-listof sequence?)]
[result sequence?])
Questo contratto garantisce due cose. Prima di tutto, l'argomento proc
deve accettare lo stesso numero di argomenti quante sono le sequenze, come menzionato sopra. Inoltre, richiede anche che quella funzione restituisca sempre un singolo valore, poiché le funzioni Racket possono restituire più valori.
Questi invarianti sarebbe molto difficile da controllare all'interno del corpo della funzione in quanto, in particolare con il secondo invariante, devono essere ritardata fino funzione stessa è applicata . Deve anche essere verificato con ogni invocazione della funzione. Contratti, d'altra parte, avvolgere la funzione e gestirlo automaticamente.
Vuoi sempre codificare ogni singolo invariante di una funzione in un contratto? Probabilmente no. Ma se vuoi quel livello extra di controllo, è disponibile ->i
.