Prendendo una tipizzazione strong e statica a un estremo? [duplicare]

9

È comune nella tipizzazione strong e statica utilizzare tipi diversi anche per variabili con tipi semplici e primitivi per facilitare l'analisi statica e indicare l'intento del programmatore. Un colore e un punto nello spazio 3D potrebbero essere entrambi rappresentati da un array di 3 float, ma con nomi di tipi diversi. In C, molti tipi comuni sono semplicemente typedef inte.

Mi chiedo: fino a che punto è pratico prendere questo? Se stai scrivendo una funzione che ha un valore compreso tra 0 e 1 (forse rappresenta una distribuzione di probabilità) crei un tipo separato? Che dire di una funzione che deve assumere un numero intero diverso da zero? Sarebbe ragionevole creare un tipo nonZeroInt, non ai fini dell'incapsulamento, ma semplicemente per la sicurezza del tipo?

Puoi prendere questo arbitrariamente lontano, codificando tutte le precondizioni di funzioni nel sistema di tipi. Ad esempio, è possibile definire un tipo "primeNumber" per l'input per una funzione che richiede solo un primo. Una funzione che deve contenere due interi senza fattori comuni potrebbe essere modificata per prendere un argomento di tipo "coprimePair".

Le due funzioni ipotetiche sopra sarebbero presumibilmente parte di una libreria che include funzioni per generare numeri primi e coppie coprimi e restituirle con il tipo appropriato. Se volessi richiamare una funzione in quella libreria che richiede un numero primo, e non l'ho ottenuta dalla funzione di generazione primaria inclusa, dovrei eseguire il cast esplicito di un int, essenzialmente costringendomi a fare un controllo di realtà e chiedere me stesso, "Sono sicuro che questa variabile sarà sempre in primo piano?"

La mia domanda specifica è questa: in che misura questa filosofia con successo è stata presa nella pratica? Ci sono linguaggi di programmazione, o testi di programmazione ben rispettati, che incoraggiano una filosofia che dice: ogni funzione che non può ospitare l'intera gamma di qualsiasi tipo esistente come suo input, e restituire un risultato significativo per tutti i possibili input, dovrebbe invece definire un nuovo tipo?

    
posta Josh 26.05.2015 - 05:50
fonte

3 risposte

5

I commenti di Jack e tp1 (che in realtà dovrebbero essere le risposte) spiegano già come questo sia implementato nei linguaggi funzionali.

La mia risposta aggiunge un punto sui linguaggi non funzionali, in particolare quello piuttosto popolare nel settore dello sviluppo: C #.

In linguaggi come C # o Java, è effettivamente una pratica corrente creare tipi quando è necessario limitare un po 'i valori. Se l'unica cosa di cui hai bisogno è un intero positivo, finirai con la classe PositiveInt , ma in generale quei wrapper rifletteranno un po 'di più la logica di business ( ProductPrice , RebatePercentage , ecc.), Di nuovo con convalida all'interno (un prezzo del prodotto dovrebbe essere superiore a zero, una percentuale di sconto non accetta un valore superiore a 100, ecc.)

I vantaggi e gli svantaggi e il rischio di andare troppo lontano con questi tipi sono stati recentemente discussi qui .

Una volta che hai il tuo wrapper, puoi iniziare ad aggiungere la logica ad esso, e specialmente la validazione. Fondamentalmente, poiché il wrapper nasconde il valore che avvolge, la logica di validazione sarà localizzata nel costruttore e può essere così semplice come:

if (value > 100)
{
    throw new ArgumentOutOfRangeException("value", "The allowed range is (0..100].");
}

Un'altra possibilità offerta da C # è quella di utilizzare contratti di codice , che offre diversi vantaggi:

  • Il controllo statico cattura l'uso improprio di quei wrapper prima di raggiungere l'errore durante il runtime,

  • Anche i contratti di codice vengono applicati durante il runtime, si è sicuri che il contratto non sarà utilizzato in modo improprio anche se il controllo statico è disabilitato (o gli avvisi sono stati ignorati dagli sviluppatori),

  • Con gli invarianti che controllano il valore avvolto, non c'è modo di assegnare un valore non valido (con la limitazione che i controlli siano eseguiti quando un metodo inizia o termina, e non ad ogni passo durante l'esecuzione del metodo) ,

  • L'integrazione di Visual Studio rende il codice autodocumentante fornendo suggerimenti sui contratti ai chiamanti.

È con successo preso in pratica? Bene, ci sono migliaia di metodi all'interno dello stesso .NET Framework che contengono contratti di codice. Per il codice aziendale, si tratta soprattutto di quanto sia critico il codice. Se le conseguenze di un fallimento sono costose, potrebbe essere molto interessante utilizzare i contratti di codice. D'altra parte, i contratti di codice hanno un costo notevole in termini di tempo dello sviluppatore (assicurando che tutti i contratti funzionino bene su progetti tutt'altro che piccoli richiedono molto di tempo) e potrebbero non essere una buona idea per i progetti che non hanno necessariamente bisogno di questo livello di affidabilità.

Questo risponde anche alla tua altra domanda:

I'm wondering: just how far is it practical to take this?

Questa è una domanda di rigore rispetto alla flessibilità.

  • Se hai bisogno di sviluppare molto velocemente e accetti il rischio di avere errori di runtime, scegli un linguaggio tipizzato debolmente. Il vantaggio di scrivere meno codice, ad esempio print(123) supera il rischio di problemi che possono essere più o meno difficili da eseguire il debug, ad esempio 123 + "4" (potrebbe risultare in 127 o "1234" ?)

    In questo caso, i tipi non esisteranno o saranno gestiti implicitamente dalla lingua / framework. Mentre si può ancora eseguire la convalida dell'intervallo (ad esempio per disinfettare l'input dell'utente), sembrerebbe strano fare tale convalida al di fuori delle interfacce con il mondo esterno.

  • Se lavori su un software vitale, utilizzerai una prova formale che richiederà molto tempo, ma assicurati che non ci siano errori nel codice.

    In questo caso, è probabile che i tipi abbiano una convalida rigorosa: intervallo e precisione per valori numerici, lunghezza e caratteri consentiti per le stringhe, ecc.

  • Nel frattempo, scegli l'approccio che corrisponde maggiormente alle tue esigenze. Per la maggior parte dei software, i contratti di codice sarebbero eccessivi. Ma per la maggior parte dei software, avere un controllo di base all'interno di un type wrapper (come Percentage ) può essere una buona idea. Senza andare agli estremi (come discusso nel link già fornito sopra) di creare classi per tutto , potrebbe essere un buon compromesso avere alcuni tipi generici come Range<T> o LengthLimitedString che non sono così difficili da implementare in linguaggi come C # o Java.

risposta data 26.05.2015 - 09:00
fonte
10

Ero solito scrivere software del mondo reale in questo modo ad Ada, per aerei militari. Abbiamo usato tipi separati non solo per gli intervalli, ma per unità e indici di array. Ad esempio, una distanza in metri era di un tipo diverso da una distanza in piedi. L'indice in un array di 100 elementi aveva un tipo diverso rispetto a un indice in un array di 10 elementi.

In primo luogo, il bene. Un bel po 'di bug sono stati effettivamente catturati al momento della compilazione. I bug di runtime venivano solitamente catturati più vicino all'origine. Ad esempio, un indice di matrice fuori limite verrebbe catturato nel momento in cui l'indice è stato assegnato per la prima volta, piuttosto che quando si tenta di dereferenziarlo. Inoltre, ci sono alcuni vantaggi di velocità, perché il runtime non deve controllare le cose che il sistema di tipi ha controllato in modo statico.

C'erano anche una serie di inconvenienti:

  • Ampio volume di errori di compilazione, spesso con messaggi strani. Questo è l'effetto collaterale di catturare bug in fase di compilazione. Ti senti come se stessi sempre combattendo il compilatore. D'altra parte, spesso ha funzionato la prima volta quando hai corretto tutti gli errori del compilatore.
  • Un ton di tipo annotazioni.
  • Uso pesante di farmaci generici. Avevo circa due anni nel mio lavoro C ++ quando ho dovuto prima cercare la sintassi per creare la mia classe personalizzata. Ho fatto l'equivalente Ada ogni giorno.
  • Era necessario un solido standard di codifica e un processo di revisione tra pari per evitare che i programmatori prendessero le scorciatoie meno sicure.
  • È stato difficile ottenere rapidamente nuovi programmatori che erano abituati a tipi più sciolti.

Per qualcosa di importante per la sicurezza, ne è valsa decisamente la pena. Tuttavia, penso che preferirei un linguaggio con inferenza di tipo e un strong controllo del programmatore sugli impliciti, come Scala, in cui potresti ottenere molta sicurezza con meno digitazioni.

    
risposta data 26.05.2015 - 15:56
fonte
2

Ecco come è stato fatto in haskell:

data Prime = Prime Int
primeFromInt :: Int -> Maybe Prime
primeFromInt a | isPrime a = Just (Prime a)
primeFromInt _ = Nothing

Tieni presente che consentirà comunque di affermare che un int è un numero primo:

Prime 10

Anche se 10 non è primo, il compilatore semplicemente non può rilevare questo problema. Invece se lo scrivi in un altro modo

primeFromInt 10

Questo rifiuterà bene il 10 e non ci darà nulla.

    
risposta data 27.05.2015 - 15:07
fonte

Leggi altre domande sui tag