haskell type-conversion type-safety

haskell - Uso de ''inseguroCuerpo''



type-conversion type-safety (3)

La única vez que me sentí obligado a utilizar unsafeCoerce fue en números naturales finitos.

{-# LANGUAGE DataKinds, GADTs, TypeFamilies, StandaloneDeriving #-} data Nat = Z | S Nat deriving (Eq, Show) data Fin (n :: Nat) :: * where FZ :: Fin (S n) FS :: Fin n -> Fin (S n) deriving instance Show (Fin n)

Fin n es una estructura de datos de enlace único que está estáticamente asegurada para que sea más pequeña que el número natural de nivel de tipo n mediante el cual se parametriza.

-- OK, 1 < 2 validFin :: Fin (S (S Z)) validFin = FS FZ -- type error, 2 < 2 is false invalidFin :: Fin (S (S Z)) invalidFin = FS (FS FZ)

Fin se puede utilizar para indexar con seguridad en varias estructuras de datos. Es bastante estándar en los idiomas con subtítulos dependientes, aunque no en Haskell.

A veces queremos convertir un valor de Fin n en Fin m donde m es mayor que n .

relaxFin :: Fin n -> Fin (S n) relaxFin FZ = FZ relaxFin (FS n) = FS (relaxFin n)

relaxFin es una relaxFin no relaxFin por definición, pero aún se requiere recorrer el valor para que los tipos se puedan extraer. Entonces, podríamos usar unsafeCoerce lugar de relaxFin . Los aumentos de velocidad más pronunciados pueden resultar de la coerción de estructuras de datos más grandes que contienen Fin -s (por ejemplo, podría tener términos lambda con Fin -s como variables enlazadas).

Este es un ejemplo ciertamente exótico, pero me parece interesante en el sentido de que es bastante seguro: no puedo pensar en formas para que las bibliotecas externas o el código de usuario seguro lo estropeen. Sin embargo, podría estar equivocado y estaría ansioso por escuchar sobre posibles problemas de seguridad.

En Haskell, hay una función llamada unsafeCoerce , que convierte cualquier cosa en cualquier otro tipo de cosa. ¿Para qué se usa esto exactamente? Por ejemplo, ¿por qué quisiéramos transformar las cosas unas a otras de una manera "insegura"?

Proporcione un ejemplo de una forma en que unsafeCoerce se usa el unsafeCoerce . Un enlace a Hackage ayudaría. El código de ejemplo en la pregunta de alguien no.


No hay uso de unsafeCoerce que realmente pueda recomendar, pero puedo ver que en algunos casos tal cosa podría ser útil.

El primer uso que me viene a la mente es la implementación de las rutinas relacionadas con Typeable . En particular, cast :: (Typeable a, Typeable b) => a -> Maybe b logra un comportamiento seguro de tipo, por lo que es seguro de usar, sin embargo, tiene que jugar trucos sucios en su implementación.

Tal vez unsafeCoerce puede ser útil al importar subrutinas FFI para obligar a los tipos a coincidir. Después de todo, FFI ya permite importar funciones impuras de C como puros, por lo que es intrínsecamente usafe. Tenga en cuenta que "inseguro" no significa imposible de usar, sino simplemente "poner la carga de la prueba en el programador".

Finalmente, pretenda que sortBy no existió. Considera entonces este ejemplo:

-- Like Int, but using the opposite ordering newtype Rev = Rev { unRev :: Int } instance Ord Rev where compare (Rev x) (Rev y) = compare y x sortDescending :: [Int] -> [Int] sortDescending = map unRev . sort . map Rev

El código anterior funciona, pero se siente tonto en mi humilde opinión. Realizamos dos map utilizando funciones como Rev,unRev que sabemos que no son operaciones en tiempo de ejecución. Entonces escaneamos la lista dos veces sin ninguna razón, pero la de convencer al compilador de que use la instancia Ord correcta.

El impacto en el rendimiento de estos mapas debe ser pequeño, ya que también ordenamos la lista. Sin embargo, es tentador volver a escribir el map Rev como unsafeCoerce :: [Int]->[Rev] y ahorrar algo de tiempo.

Tenga en cuenta que tiene una función de coerción

castNewtype :: IsNewtype t1 t2 => f t2 -> f t1

donde la restricción significa que t1 es un nuevo tipo para t2 ayudaría, pero sería bastante peligroso. Considerar

castNewtype :: Data.Set Int -> Data.Set Rev

Lo anterior causaría que la estructura de datos invariante se rompa, ¡ya que estamos cambiando el orden debajo! Dado que Data.Set se implementa como un árbol de búsqueda binario, causaría un daño bastante grande.


unsafeCoerce permite convencer al sistema de tipo de cualquier propiedad que le guste. Por lo tanto, solo es "seguro" exactamente cuando puede estar completamente seguro de que la propiedad que está declarando es verdadera. Entonces, por ejemplo:

unsafeCoerce True :: Int

es una violación y puede llevar a un comportamiento débil y mal ejecutado.

unsafeCoerce (3 :: Int) :: Int

está (obviamente) bien y no dará lugar a una mala conducta en el tiempo de ejecución.

Entonces, ¿qué es un uso no trivial de unsafeCoerce ? Digamos que tenemos un tipo existencial ligado a las clases de tipos

module MyClass ( SomethingMyClass (..), intSomething ) where class MyClass x where {} instance MyClass Int where {} data SomethingMyClass = forall a. MyClass a => SomethingMyClass a

Digamos también, como se señala aquí, que la clase de tipos MyClass no se exporta y, por lo tanto, nadie más puede crear instancias de ella. De hecho, Int es lo único que lo ejemplifica y lo único que lo hará.

Ahora cuando el patrón coincida para destruir un valor de SomethingMyClass , seremos capaces de sacar un "algo" de adentro

foo :: SomethingMyClass -> ... foo (SomethingMyClass a) = -- here we have a value `a` with type `exists a . MyClass a => a` -- -- this is totally useless since `MyClass` doesn''t even have any -- methods for us to use! ...

Ahora, en este punto, como sugiere el comentario, el valor que hemos extraído no tiene información de tipo: ha sido "olvidado" por el contexto existencial. Podría ser absolutamente cualquier cosa que ejemplifique MyClass .

Por supuesto, en esta situación tan particular , sabemos que lo único que implementa MyClass es Int . Entonces, nuestro valor a debe tener realmente tipo Int . Nunca podríamos convencer al taquimecanista de que esto es cierto, pero debido a una prueba externa sabemos que sí lo es.

Por lo tanto, podemos (muy cuidadosamente)

intSomething :: SomethingMyClass -> Int intSomething (SomethingMyClass a) = unsafeCoerce a -- shudder!

Ahora, espero haber sugerido que esta es una idea terrible y peligrosa, pero también puede dar una idea de qué tipo de información podemos aprovechar para saber cosas que el contador no puede.

En situaciones no patológicas, esto es raro. Aún más raro es una situación en la que el uso de algo que conocemos y el tipochelador no es en sí mismo no es patológico. En el ejemplo anterior, debemos estar completamente seguros de que nadie amplía nuestro módulo MyClass para instanciar más tipos en MyClass contrario, nuestro uso de unsafeCoerce vuelve inseguro al instante.

> instance MyClass Bool where {} > intSomething (SomethingMyClass True) 6917529027658597398

¡Parece que nuestro compilador interno está goteando!

Un ejemplo más común donde este tipo de comportamiento podría ser valioso es cuando se usan envoltorios de tipo nuevo. Es una idea bastante común que podríamos ajustar un tipo en un contenedor de tipo newtype para especializar sus definiciones de instance .

Por ejemplo, Int no tiene una definición de Monoid porque hay dos monoides naturales sobre Int : sumas y productos. En cambio, usamos envoltorios de tipo nuevo para ser más explícitos.

newtype Sum a = Sum { getSum :: a } instance Num a => Monoid (Sum a) where mempty = Sum 0 mappend (Sum a) (Sum b) = Sum (a+b)

Ahora, normalmente el compilador es bastante inteligente y reconoce que puede eliminar todos esos constructores Sum para producir un código más eficiente. Lamentablemente, hay momentos en que no puede, especialmente en situaciones altamente polimórficas.

Si (a) sabe que algún tipo a es realmente solo un nuevo tipo b y (b) sabe que el compilador es incapaz de deducir esto por sí mismo, entonces es posible que desee hacer

unsafeCoerce (x :: a) :: b

por una pequeña ganancia de eficiencia. Esto, por ejemplo, ocurre con frecuencia en la lens y se expresa en el módulo profunctors de profunctors , una dependencia de la lens .

Pero permítame nuevamente sugerirle que realmente necesita saber lo que está sucediendo antes de usar unsafeCoerce que esto no es muy seguro.

Una última cosa para comparar es el " cast seguro" disponible en Data.Typeable . Esta función se parece un poco a unsafeCoerce , pero con mucha más ceremonia.

unsafeCoerce :: a -> b cast :: (Typeable a, Typeable b) => a -> Maybe b

Lo cual, se podría pensar que se está implementando utilizando unsafeCoerce y una función typeOf :: Typeable a => a -> TypeRep donde TypeRep son incontrolables, tokens en tiempo de ejecución que reflejan el tipo de un valor. Entonces nosotros tenemos

cast :: (Typeable a, Typeable b) => a -> Maybe b cast a = if (typeOf a == typeOf b) then Just b else Nothing where b = unsafeCoerce a

Por lo tanto, el cast puede garantizar que los tipos de b sean realmente iguales en tiempo de ejecución, y puede decidir devolver Nothing si no lo están. Como ejemplo:

{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE ExistentialQuantification #-} data A = A deriving (Show, Typeable) data B = B deriving (Show, Typeable) data Forget = forall a . Typeable a => Forget a getAnA :: Forget -> Maybe A getAnA (Forget something) = cast something

que podemos ejecutar de la siguiente manera

> getAnA (Forget A) Just A > getAnA (Forget B) Nothing

Entonces, si comparamos este uso de cast con unsafeCoerce , vemos que puede lograr algo de la misma funcionalidad. En particular, nos permite redescubrir información que puede haber sido olvidada por ExistentialQuantification . Sin embargo, el cast comprueba manualmente los tipos en tiempo de ejecución para garantizar que sean realmente iguales y, por lo tanto, no se pueden usar de forma insegura. Para hacer esto, exige que tanto el tipo de origen como el de destino permitan la reflexión en tiempo de ejecución de sus tipos a través de la clase Typeable .