2009-06-13 8 views
56

Lo so newtype è più spesso rispetto a data in Haskell, ma sto ponendo questo confronto da un punto di vista più progettuale che da un problema tecnico.Haskell type vs. newtype rispetto alla sicurezza del tipo

In linguaggi imperitive/OO, c'è l'anti-modello "primitive obsession", dove l'uso prolifico di tipi primitivi riduce il tipo di sicurezza di un programma e introduce accidentalmente intercambiabilità dei valori stessi tipizzato, altrimenti destinati a scopi diversi . Ad esempio, molte cose possono essere una stringa, ma sarebbe bello se un compilatore potesse sapere, staticamente, che intendiamo essere un nome e che intendiamo essere la città in un indirizzo.

Quindi, quanto spesso, i programmatori Haskell impiegano newtype per dare distinzioni di tipo a valori altrimenti primitivi? L'utilizzo di type introduce un alias e fornisce una semantica di leggibilità più chiara del programma, ma non impedisce l'interscambio accidentale di valori. Mentre imparo haskell, noto che il sistema dei tipi è potente come qualsiasi altro che abbia mai incontrato. Pertanto, penserei che questa sia una pratica naturale e comune, ma non ho visto molto o nessuna discussione sull'uso di newtype in questa luce.

Ovviamente molti programmatori fanno le cose in modo diverso, ma questo è assolutamente comune in haskell?

+0

Hrm ... sembra che non sia possibile contrassegnare più di una risposta come accettata. Speravo di accettare in qualche modo una ragionevole rappresentazione delle diverse opinioni su questo tema ... – StevenC

risposta

52

I principali usi per newtypes sono:

  1. Per definire le istanze alternative per i tipi.
  2. Documentazione.
  3. Garanzia di correttezza dei dati/formato.

Sto lavorando a un'applicazione in questo momento in cui utilizzo estesamente i nuovi tipi. newtypes in Haskell sono un concetto puramente in fase di compilazione. Per esempio. con i wrapper qui sotto, unFilename (Filename "x") compilato con lo stesso codice di "x". C'è assolutamente zero hit di run-time. C'è con i tipi data. Questo lo rende un ottimo modo per raggiungere gli obiettivi sopra elencati.

-- | A file name (not a file path). 
newtype Filename = Filename { unFilename :: String } 
    deriving (Show,Eq) 

Non voglio trattarlo accidentalmente come un percorso di file. Non è un percorso di file. È il nome di un file concettuale da qualche parte nel database.

È molto importante che gli algoritmi facciano riferimento alla cosa giusta, i nuovi tipi aiutano in questo. È anche molto importante per la sicurezza, ad esempio, prendere in considerazione il caricamento di file su un'applicazione web. Ho questi tipi:

-- | A sanitized (safe) filename. 
newtype SanitizedFilename = 
    SanitizedFilename { unSafe :: String } deriving Show 

-- | Unique, sanitized filename. 
newtype UniqueFilename = 
    UniqueFilename { unUnique :: SanitizedFilename } deriving Show 

-- | An uploaded file. 
data File = File { 
    file_name  :: String   --^Uploaded file. 
    ,file_location :: UniqueFilename --^Saved location. 
    ,file_type  :: String   --^File type. 
    } deriving (Show) 

Supponiamo che io abbia questa funzione che pulisce un nome di file da un file che è stato caricato:

-- | Sanitize a filename for saving to upload directory. 
sanitizeFilename :: String   --^Arbitrary filename. 
       -> SanitizedFilename --^Sanitized filename. 
sanitizeFilename = SanitizedFilename . filter ok where 
    ok c = isDigit c || isLetter c || elem c "-_." 

Ora da quel genero un nome file univoco:

-- | Generate a unique filename. 
uniqueFilename :: SanitizedFilename --^Sanitized filename. 
       -> IO UniqueFilename --^Unique filename. 

È pericoloso generare un nome file univoco da un nome file arbitrario, deve essere prima disinfettato.Allo stesso modo, un nome file univoco è quindi sempre sicuro per estensione. Ora posso salvare il file su disco e inserire il nome del file nel mio database, se lo desidero.

Ma può anche essere fastidioso dover avvolgere/scartare molto. A lungo termine, lo considero ne vale la pena soprattutto per evitare discrepanze di valore. ViewPatterns aiutano un po ':

-- | Get the form fields for a form. 
formFields :: ConferenceId -> Controller [Field] 
formFields (unConferenceId -> cid) = getFields where 
    ... code using cid .. 

Forse si dirà che non imballato in una funzione è un problema - quello che se si passa a una funzione cid torto? Non è un problema, tutte le funzioni che utilizzano un ID conferenza utilizzeranno il tipo ConferenceId. Ciò che emerge è una sorta di sistema di contratto a livello di funzione a funzione che è forzato in fase di compilazione. Molto carino. Quindi sì, lo uso più spesso che posso, specialmente nei grandi sistemi.

+0

Questa è roba incredibilmente bella, Chris. Ho appena usato questo per una soluzione di tipo classe per Real World Haskell capitolo 8 esercizio 2 dal primo gruppo di esercizi. Si richiede di fornire un modo per selezionare la corrispondenza glob senza distinzione tra maiuscole e minuscole. Grazie :) –

+0

Come è il ViewPattern nell'ultimo esempio diverso da '(ConferenceID cid)'? – Dan

+2

Nel mio caso non esporto il costruttore perché non voglio creare valori arbitrari da qualsiasi vecchio intero, dovrebbe provenire solo dal database. Posso scartarne uno in modo sicuro e usare quello intero. –

10

Penso che sia abbastanza comune usare newtype per le distinzioni di tipo. In molti casi ciò è dovuto al fatto che si desidera fornire istanze di classi di tipi diverse o nascondere le implementazioni, ma semplicemente voler proteggere dalle conversioni accidentali è anche una ragione ovvia per farlo.

19

Penso che questo sia principalmente una questione di situazione.

Considerare i percorsi. Il preludio standard ha "tipo FilePath = String" perché, per comodità, si desidera avere accesso a tutte le operazioni stringa ed elenco. Se avessi "newtype FilePath = FilePath String" allora avresti bisogno di filePathLength, filePathMap e così via, altrimenti useresti per sempre le funzioni di conversione.

D'altra parte, considerare le query SQL. SQL injection è un buco di sicurezza comune, quindi ha senso avere qualcosa di simile

newtype Query = Query String 

e quindi aggiungere funzioni extra che converte una stringa in una query (o frammento di query) per sfuggire virgolette, oppure compilare spazi vuoti in un modello allo stesso modo. In questo modo non è possibile convertire accidentalmente un parametro utente in una query senza passare attraverso la funzione di escape dell'offerta.

+0

In risposta all'esempio del percorso file, la domanda è più nel contesto del design che stai facendo, e meno su ciò che è già stato progettato dove non hai il controllo. Nella prima situazione, il consumatore del tuo modulo/funzione/qualunque cosa non vedrà il codice per ottenere la primitiva. In quest'ultima situazione, è al peggio quella chiamata a far uscire la primitiva appena prima della chiamata. D'altro canto, questo è esattamente il motivo per cui ho chiesto: per avere un'idea di cosa pensano i diversi programmatori di haskell sulla scelta del design. Devo ammettere che sono una persona che si sporge verso la sicurezza per comodità. – StevenC

+0

Come ho detto però, dal momento che volevo avere un senso di diff. pratiche nella cultura di haskell, la tua risposta è ancora valida. Non ho ancora finito qui. :) – StevenC

+3

Capisco che stai prendendo in considerazione la tua pratica progettuale: volevo solo dare un paio di esempi pratici. Il costo di pronunciare "newtype FilePath" è in fase di programmazione; le funzioni di conversione servono solo a mantenere il type checker felice e non hanno alcuna implementazione. Il punto principale è che se si converte ripetutamente dentro e fuori il tuo nuovo tipo, allora non hai una vera sicurezza extra, solo un sacco di chiamate di funzioni offuscanti. Quindi, quando si progetta una libreria è necessario pensare al punto di vista dei programmatori dell'applicazione. –

14

Per le dichiarazioni semplici X = Y, type è la documentazione; newtype è tipo di controllo; questo è il motivo per cui newtype viene confrontato con data.

Uso abbastanza frequentemente lo newtype per il solo scopo che si descrive: accertarsi che qualcosa che è archiviato (e spesso manipolato) nello stesso modo di un altro tipo non venga confuso con qualcos'altro. In questo modo funziona come una dichiarazione data leggermente più efficiente; non c'è alcun motivo particolare per sceglierne uno rispetto all'altro. Si noti che con l'estensione GeneralizedNewtypeDeriving di GHC, per entrambi è possibile derivare automaticamente classi come Num, consentendo di aggiungere o sottrarre le temperature o lo yen come è possibile con lo Int s o qualsiasi altra cosa sottostante. Uno vuole essere un po 'attento con questo, tuttavia; in genere uno non moltiplica la temperatura per un'altra temperatura!

Per avere un'idea di quanto spesso si utilizzano queste cose, in un ragionevolmente grande progetto su cui sto lavorando in questo momento, ho circa 122 usi di data, 39 usi di newtype, e 96 usi di type.

Ma il rapporto, per quanto riguarda i tipi di "semplici" sono interessati, è un po 'più vicino di che dimostra, perché 32 di quei 96 usi di type sono in realtà alias per i tipi di funzioni, come ad esempio

type PlotDataGen t = PlotSeries t -> [String] 

Noterai due ulteriori complessità qui: innanzitutto, è in realtà un tipo di funzione, non solo un semplice alias X = Y e in secondo luogo che è parametrizzato: PlotDataGen è un costruttore di tipi che applico a un altro tipo per creare un nuovo tipo, come PlotDataGen (Int,Double) . Quando si inizia a fare questo genere di cose, type non è più solo documentazione, ma in realtà è una funzione, sebbene a livello di testo anziché a livello di dati.

newtype viene occasionalmente utilizzato dove non può essere type, ad esempio dove è necessaria una definizione di tipo ricorsivo, ma trovo che sia ragionevolmente raro. Quindi sembra che, su questo particolare progetto, almeno il 40% delle mie definizioni di tipo "primitivo" sono newtype se il 60% sono type s. Molte delle definizioni newtype erano tipi e venivano definitivamente convertite per i motivi esatti che hai menzionato.

Quindi, insomma, si tratta di un linguaggio frequente.