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
.