2009-06-13 21 views
56

newtype más a menudo se compara con data en Haskell, pero estoy planteando esta comparación más desde un punto de vista de diseño que como un problema técnico.tipo Haskell vs. newtype con respecto al tipo de seguridad

En idiomas imperitivos/OO, existe el patrón anti-patrón "primitive obsession", donde el uso prolífico de tipos primitivos reduce la seguridad de tipo de un programa e introduce intercambiabilidad accidental de valores del mismo tipo, de lo contrario destinados a diferentes propósitos . Por ejemplo, muchas cosas pueden ser un String, pero sería bueno que un compilador supiera, estáticamente, lo que queremos decir es un nombre y que queremos decir que es la ciudad en una dirección.

Entonces, ¿con qué frecuencia los programadores de Haskell emplean newtype para dar distinciones de tipo a valores primitivos? El uso de type introduce un alias y proporciona una semántica más clara de legibilidad del programa, pero no evita intercambios accidentales de valores. Cuando lo aprendo, me doy cuenta de que el sistema de tipos es tan poderoso como cualquiera de los que he encontrado. Por lo tanto, creo que esta es una práctica natural y común, pero no he visto mucho o ninguna discusión sobre el uso de newtype bajo esta luz.

Por supuesto, muchos programadores hacen las cosas de manera diferente, pero ¿es esto algo común en Haskell?

+0

Hrm ... Parece que no puedo marcar más de una respuesta como aceptada. Esperaba de alguna manera aceptar una representación razonable de las diferentes opiniones sobre este tema ... – StevenC

Respuesta

52

Los principales usos para Newtypes son:

  1. Para la definición de casos alternativos para los tipos.
  2. Documentación.
  3. Garantía de corrección de datos/formatos.

Estoy trabajando en una aplicación en este momento en la que uso newtypes de manera exhaustiva. newtypes en Haskell son un concepto puramente de tiempo de compilación. P.ej. con los desempaquetadores a continuación, unFilename (Filename "x") compilados con el mismo código que "x". No hay absolutamente ningún hit de tiempo de ejecución. Hay con data tipos. Esto lo convierte en una forma muy agradable de lograr los objetivos enumerados anteriormente.

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

No quiero tratar accidentalmente esto como una ruta de archivo. No es una ruta de archivo. Es el nombre de un archivo conceptual en algún lugar de la base de datos.

Es muy importante que los algoritmos hagan referencia a lo correcto, los nuevos tipos ayudan con esto. También es muy importante para la seguridad, por ejemplo, considerar la carga de archivos a una aplicación web. Tengo estos tipos:

-- | 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) 

Supongamos que tengo esta función que limpia un nombre de archivo de un archivo que se ha subido:

-- | 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 "-_." 

Ahora a partir de ese genero un nombre de archivo único:

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

Es peligroso generar un nombre de archivo único a partir de un nombre de archivo arbitrario, primero debe desinfectarse.Del mismo modo, un nombre de archivo único siempre es seguro por extensión. Puedo guardar el archivo en el disco ahora y poner ese nombre de archivo en mi base de datos si quiero.

Pero también puede ser molesto tener que envolver/desenvolver un montón. A la larga, creo que vale la pena, especialmente para evitar desajustes de valores. ViewPatterns ayudar un poco:

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

Tal vez quiera decir que desenvolver en una función es un problema - lo que si se pasa a una función cid erróneamente? No es un problema, todas las funciones que usan una identificación de conferencia usarán el tipo ConferenceId. Lo que surge es una especie de sistema de contrato de función a función que se ve forzado en tiempo de compilación. Bastante agradable. Así que sí lo uso tan a menudo como puedo, especialmente en sistemas grandes.

+0

Esto es increíblemente genial Chris. Acabo de usar esto para una solución de clase de tipo para Real World Haskell capítulo 8 ejercicio 2 del primer conjunto de ejercicios. Pide que se proporcione una forma de seleccionar una coincidencia de comodines que no distinga entre mayúsculas y minúsculas. Gracias :) –

+0

¿Cómo es el ViewPattern en el último ejemplo diferente de '(ConferenceID cid)'? – Dan

+2

En mi caso, no exporto el constructor porque no quiero crear valores arbitrarios a partir de ningún entero antiguo, solo debe venir de la base de datos. Puedo desenvolver uno de forma segura y usar ese entero. –

10

Creo que es bastante común usar newtype para las distinciones de tipo. En muchos casos, esto se debe a que desea dar diferentes instancias de clase de tipo u ocultar las implementaciones, pero simplemente querer proteger contra las conversiones accidentales también es una razón obvia para hacerlo.

19

Creo que esto es principalmente una cuestión de situación.

Considere los nombres de las rutas. El preludio estándar tiene "tipo FilePath = String" porque, como una cuestión de conveniencia, desea tener acceso a todas las operaciones de cadenas y listas. Si tuviera "newtype FilePath = FilePath String", necesitaría filePathLength, filePathMap y demás, de lo contrario, estaría siempre utilizando las funciones de conversión.

Por otro lado, considere las consultas SQL. inyección SQL es un agujero de seguridad común, por lo que tiene sentido tener algo así como

newtype Query = Query String 

y luego añadir funciones adicionales que convertirán una cadena en una consulta (o fragmento de consulta) al escapar comillas, o rellenar espacios en blanco en una plantilla de la misma manera. De esta forma, no puede convertir accidentalmente un parámetro de usuario en una consulta sin pasar por la función de escape de comillas.

+0

En respuesta al ejemplo de ruta de archivo, la pregunta está más en el contexto del diseño que estás haciendo, y menos en lo que ya se diseñó donde no tienes control En la situación anterior, el consumidor de su módulo/función/lo que sea no verá el código para obtener el primitivo. En la última situación, en el peor de los casos es la única llamada para recuperar el primitivo justo antes de la llamada. Por otro lado, esa es exactamente la razón por la que pedí: tener una idea de lo que piensan los diferentes programadores de Haskell sobre la elección del diseño. Es cierto que soy una persona que se inclina hacia la seguridad por sobre la conveniencia. – StevenC

+0

Como dije, sin embargo, ya que quería tener una idea de diff. prácticas en la cultura haskell, tu respuesta sigue siendo valiosa. No he terminado aquí todavía. :) – StevenC

+3

Entiendo que está considerando su propia práctica de diseño: solo quería dar un par de ejemplos prácticos. El costo de decir "newtype FilePath" es en tiempo de programador; las funciones de conversión son solo para mantener feliz al verificador de tipos y no tener implementación. El punto principal es que si se convierte repetidamente dentro y fuera de su nuevo tipo, entonces no tiene seguridad real, solo un montón de llamadas de función ofuscadoras. Por lo tanto, al diseñar una biblioteca, debe pensar en el punto de vista de los programadores de la aplicación. –

14

Para declaraciones simples X = Y, type es documentación; newtype es comprobación de tipo; esta es la razón por newtype se compara con data.

Con bastante frecuencia uso newtype para el fin que usted describe: asegurar que algo que se almacena (y, a menudo, se manipula) de la misma manera que otro tipo no se confunda con otra cosa. De esa manera, funciona igual que una declaración data ligeramente más eficiente; no hay ninguna razón en particular para elegir uno sobre el otro. Tenga en cuenta que con la extensión GeneralizedNewtypeDeriving de GHC, puede derivar automáticamente clases como Num, permitiendo que sus temperaturas o yenes se agreguen y restan de la misma manera que con el Int o lo que se encuentre debajo de ellos. Uno quiere ser un poco cuidadoso con esto, sin embargo; ¡típicamente uno no multiplica una temperatura por otra temperatura!

Para tener una idea de con qué frecuencia se utilizan estas cosas, En un proyecto bastante grande que estoy trabajando en este momento, tengo alrededor de 122 usos de data, 39 usos de newtype, y 96 usos de type.

Pero la relación, en lo que se refiere a los tipos de "simples", es un poco más cerca de lo que demuestra, porque 32 de los 96 usos de type son en realidad los alias para este tipo de funciones, tales como

type PlotDataGen t = PlotSeries t -> [String] 

Notarás dos complejidades adicionales aquí: primero, es realmente un tipo de función, no solo un simple alias X = Y, y segundo que está parametrizado: PlotDataGen es un constructor de tipos que aplico a otro tipo para crear un tipo nuevo, como PlotDataGen (Int,Double) . Cuando comienza a hacer este tipo de cosas, type ya no es solo documentación, sino que en realidad es una función, aunque a nivel de tipo en lugar de a nivel de datos.

newtype se usa ocasionalmente donde type no puede ser, como cuando se necesita una definición de tipo recursivo, pero me parece razonablemente raro. Así que parece que, en este proyecto en particular, al menos, aproximadamente el 40% de mis definiciones de tipo "primitivo" son newtype sy el 60% son type s. Varias de las definiciones newtype solían ser tipos, y definitivamente se convirtieron por las razones exactas que usted mencionó.

En resumen, sí, este es un modismo frecuente.