2011-12-23 5 views
14

Desde un ByteString es un constructor con ForeignPtr:Pureza de funciones generadoras ByteString (o cualquier objeto con el componente ForeignPtr)

data ByteString = PS {-# UNPACK #-} !(ForeignPtr Word8) -- payload 
        {-# UNPACK #-} !Int                -- offset 
        {-# UNPACK #-} !Int                -- length 

Si tengo una función que devuelve ByteString, a continuación, dada una entrada, por ejemplo una constante Word8 , la función devolverá una ByteString con un valor de ForeignPtr no determinista: el administrador de memoria determinará cuál será ese valor.

Entonces, ¿eso significa que una función que devuelve ByteString no es pura? Ese no parece ser el caso, obviamente, si ha utilizado bibliotecas ByteString y Vector. Seguramente, se habría discutido ampliamente si fuera el caso (y esperemos que aparezca en la parte superior de la búsqueda de Google). ¿Cómo se aplica esa pureza?

La razón para hacer esta pregunta es que tengo curiosidad por saber cuáles son los puntos sutiles involucrados en el uso de objetos ByteString y Vector, desde la perspectiva del compilador GHC, dado el miembro ForeignPtr en su constructor.

Respuesta

18

No hay forma de observar el valor del puntero dentro del ForeignPtr desde fuera del módulo Data.ByteString; su implementación es internamente impura, pero externamente pura, porque asegura que las invariantes requeridas para ser puras se mantienen mientras no se pueda ver dentro del constructor ByteString, lo que no se puede hacer, porque no se exporta.

Esta es una técnica común en Haskell: implementar algo con técnicas inseguras bajo el capó, pero exponiendo una interfaz pura; obtienes tanto el rendimiento como la potencia que aportan las técnicas inseguras, sin comprometer la seguridad de Haskell. (Por supuesto, los módulos de aplicación pueden tener errores, pero ¿cree que sería ByteString menos probable escaparse su abstracción si se ha escrito en C? :))

En cuanto a los puntos sutiles van, si Desde la perspectiva de un usuario, no se preocupe: puede usar cualquier función que las bibliotecas ByteString y Vector exporten sin preocuparse, siempre que no comiencen con unsafe. Son bibliotecas muy maduras y bien probadas, por lo que no debería encontrarse con ningún problema de pureza, y si hace, eso es un error en la biblioteca, y debe informarlo.

En cuanto a escribir su propio código que proporciona seguridad externa con una implementación interna insegura, la regla es muy simple: mantener la transparencia referencial.

Tomando ByteString como ejemplo, las funciones para construir ByteStrings usan unsafePerformIO para asignar bloques de datos, que luego mutan y ponen en el constructor. Si exportamos el constructor, entonces el código de usuario podría obtener el ForeignPtr. ¿Esto es problemático? Para determinar si lo es, necesitamos encontrar una función pure (es decir, no en IO) que nos permita distinguir dos ForeignPtrs asignados de esta manera. Un rápido vistazo al the documentation muestra que existe tal función: instance Eq (ForeignPtr a) nos permite distinguirlos. Por lo tanto, no debemos permitir que el código de usuario acceda al ForeignPtr. La forma más fácil de hacerlo es no exportar el constructor.

En resumen: Cuando utiliza un mecanismo inseguro para implementar algo, verifique que la impureza que introduce no se puede filtrar fuera del módulo, p. inspeccionando los valores que produces con él.

En lo que respecta a los problemas del compilador, realmente no deberías preocuparte por ellos; mientras que las funciones son inseguras, no deberían permitirle hacer algo más peligroso, más allá de violar la pureza, que lo que puede hacer en la mónada IO para comenzar. Generalmente, si desea hacer algo que pueda producir realmente resultados inesperados, tendrá que hacer todo lo posible para hacerlo: por ejemplo, puede usar unsafeDupablePerformIO si puede tratar con la posibilidad de que dos hilos evalúen la mismo golpe de la forma unsafeDupablePerformIO m simultáneamente. unsafePerformIO es un poco más lento que unsafeDupablePerformIO porque evita que esto suceda. (Los zumbidos en su programa pueden evaluarse mediante dos subprocesos simultáneamente durante la ejecución normal con GHC, esto normalmente no es un problema, ya que evaluar el mismo valor puro dos veces no debería tener efectos secundarios adversos (por definición), pero al escribir código no seguro, es algo que tiene que tener en cuenta.)

El GHC documentation for unsafePerformIO (y unsafeDupablePerformIO, como he vinculado anteriormente) detalla algunos errores que puede encontrar; de manera similar, la documentación para unsafeCoerce# (que debe usarse a través de su nombre portátil, Unsafe.Coerce.unsafeCoerce).

+0

Bueno, planeo usar operaciones inseguras. Por lo tanto, esta pregunta :) Me gustaría aprender de los problemas que debería tener en cuenta, como lo fueron los escritores de la biblioteca. Esas ideas serán muy útiles al escribir nuestro propio código que necesita ser rápido, pero aún externo, puro, para extensiones paralelas y simultáneas. – Sal

+0

Ah, OK; eso no estaba claro para mí de la pregunta. Trataré de incorporar parte de esa información en mi respuesta, aunque es complicado ya que la regla básica es simplemente "garantizar la transparencia referencial desde fuera del módulo". – ehird

+0

Lo he ampliado un poco más, espero que esto ayude :) – ehird

Cuestiones relacionadas