tipos opciones modulos hacer funciones ejemplos definir como basico arreglos design haskell types type-systems

design - opciones - Tipo Haskell vs. newtype con respecto a la seguridad tipo



modulos haskell (4)

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.

Sé que newtype se newtype más a menudo con los 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.

En lenguajes imperiosos / OO, existe la " obsesión primitiva " antipatrón, donde el uso prolífico de tipos primitivos reduce la seguridad de tipo de un programa e introduce accidentalmente la intercambiabilidad de valores del mismo tipo, de otro modo 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 en este sentido.

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


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

Considera los nombres de ruta. 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. La 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 agregue funciones adicionales que convertirán una cadena en una consulta (o fragmento de consulta) escapando caracteres de comillas, o llenando 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.


Los principales usos para los nuevos tipos son:

  1. Para definir instancias alternativas para tipos.
  2. Documentación.
  3. Garantía de corrección de datos / formatos.

Estoy trabajando en una aplicación ahora en la que uso nuevos tipos de manera extensa. newtypes en Haskell son un concepto puramente de tiempo de compilación. Por ejemplo, con los unFilename (Filename "x") continuación, unFilename (Filename "x") compilado con el mismo código que "x". No hay absolutamente ningún hit de tiempo de ejecución. Hay con tipos de data . 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 de ayuda 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 eso 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 ayuda un poco:

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

Tal vez dirás que desenvolverlo en una función es un problema, ¿y si pasas cid a una función incorrectamente? 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.


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

Con bastante frecuencia uso newtype para el propósito que describes: 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 solo como una declaración de data un poco 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, ya sea que puede derivar automáticamente clases como Num , lo que permite que sus temperaturas o yenes se sumen y sustraigan al igual que con los Int o lo que se encuentra 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 la frecuencia con la que se usan estas cosas, en un proyecto razonablemente grande en el que estoy trabajando ahora, tengo aproximadamente 122 usos de data , 39 usos de newtype y 96 usos de type .

Pero la relación, en lo que respecta a los tipos "simples", está un poco más cerca de lo que demuestra, porque 32 de esos 96 usos del type son en realidad alias para tipos de funciones, como

type PlotDataGen t = PlotSeries t -> [String]

Aquí observará dos complejidades adicionales: primero, es en realidad 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 nuevo tipo, como como PlotDataGen (Int,Double) . Cuando comienza a hacer este tipo de cosas, el 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 el type no puede ser, como cuando se necesita una definición de tipo recursiva, pero considero que esto es razonablemente raro. Así que parece que, en este proyecto en particular, al menos, alrededor del 40% de mis definiciones de tipo "primitivo" son newtype y el 60% son type s. Varias de las definiciones de tipo nuevo solían ser tipos, y definitivamente se convirtieron por las razones exactas que usted mencionó.

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