【问题标题】:Use of 'unsafeCoerce'使用“不安全强制”
【发布时间】:2014-04-03 19:57:55
【问题描述】:

在 Haskell 中,有一个名为 unsafeCoerce 的函数,它可以将任何东西变成任何其他类型的东西。这究竟是做什么用的?比如,我们为什么要以这种“不安全”的方式将事物相互转化?

提供一个实际使用unsafeCoerce 的示例。 Hackage 的链接会有所帮助。某人问题中的示例代码不会。

【问题讨论】:

  • 它通常只在处理低级类型时使用,例如在使用 C 库或执行一些非常深的类型级魔法时。 99.9% 的 Haskeller 不应该使用它。
  • @bheklilr 不是重复的。他就是一个具体的例子。
  • 它确实提供了一个例子。
  • @bheklilr 除了答案继续表明这是一个糟糕的例子。
  • 重新打开,因为这确实比suggested question 更通用(即使这也是一个相关示例)。

标签: haskell type-conversion type-safety


【解决方案1】:

unsafeCoerce 让你说服类型系统相信你喜欢的任何属性。因此,只有当您可以完全确定您声明的属性是真实的时,它才是“安全的”。所以,例如:

unsafeCoerce True :: Int

是一种违规行为,可能会导致不稳定、不良的运行时行为。

unsafeCoerce (3 :: Int) :: Int

(显然)很好,不会导致运行时不当行为。


那么unsafeCoerce 的重要用途是什么?假设我们有一个类型类绑定的存在类型

module MyClass ( SomethingMyClass (..), intSomething ) where

class MyClass x where {}

instance MyClass Int where {}

data SomethingMyClass = forall a. MyClass a => SomethingMyClass a

我们还假设,正如这里所指出的,类型类MyClass 没有导出,因此没有其他人可以创建它的实例。事实上,Int 是唯一实例化它的东西,也是唯一会实例化它的东西。

现在,当我们使用模式匹配来破坏 SomethingMyClass 的值时,我们将能够从内部拉出“某物”

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!
  ...

现在,正如评论所暗示的那样,我们提取的值没有类型信息——它已被存在上下文“遗忘”。它绝对可以是任何实例化MyClass

当然,在这种非常特殊的情况下,我们知道唯一实现MyClass的是Int。所以我们的值a 必须实际上 具有Int 类型。我们永远无法让类型检查器相信这是真的,但由于外部证据,我们知道这是真的。

因此,我们可以(非常小心地)

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

现在,希望我提出这是一个可怕而危险的想法,但它也可以让我们体验一下我们可以利用哪些信息来了解类型检查器无法了解的事情。

在非病理情况下,这种情况很少见。更罕见的是使用我们知道的东西而类型检查器不知道的情况本身并不是病态的。在上面的例子中,我们必须完全确定没有人会扩展我们的 MyClass 模块来实例化更多类型到 MyClass 否则我们对 unsafeCoerce 的使用会立即变得不安全。

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

看起来我们的编译器内部正在泄漏!


使用newtype 包装器时,此类行为可能有价值的更常见示例是。这是一个相当普遍的想法,我们可以将一个类型包装在 newtype 包装器中,以便专门化其 instance 定义。

例如,Int 没有Monoid 定义,因为Ints 上有两个自然幺半群:和和积。相反,我们使用newtype 包装器更明确。

newtype Sum a = Sum { getSum :: a }

instance Num a => Monoid (Sum a) where
  mempty = Sum 0
  mappend (Sum a) (Sum b) = Sum (a+b)

现在,通常编译器非常聪明,并且认识到它可以消除所有那些Sum 构造函数,以便生成更高效的代码。可悲的是,有时它不能,尤其是在高度多态的情况下。

如果您 (a) 知道某些类型 a 实际上只是一个新类型包装的 b 并且 (b) 知道编译器无法自行推断,那么您可能想要这样做

unsafeCoerce (x :: a) :: b

略微提高效率。例如,这经常出现在lens 中,并在profunctorsData.Profunctor.Unsafe 模块中表示,这是lens 的依赖项。

但让我再次建议您在使用 unsafeCoerce 之前确实需要知道发生了什么,因为这绝不是非常不安全的。


最后要比较的是Data.Typeable 中的“typesafe cast”。这个函数看起来有点像unsafeCoerce,但是多了些仪式感。

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

其中,您可能会认为是使用unsafeCoerce 和函数typeOf :: Typeable a => a -> TypeRep 实现的,其中TypeRep 是不可伪造的,反映值类型的运行时标记。然后我们有

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

因此,cast 能够确保ab 的类型在运行时确实相同,如果它们不一样,它可以决定返回Nothing .举个例子:

{-# 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

我们可以如下运行

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

因此,如果我们将cast 的用法与unsafeCoerce 进行比较,我们会发现它可以实现一些相同的功能。特别是,它使我们能够重新发现可能被ExistentialQuantification 遗忘的信息。但是,cast 在运行时手动检查类型以确保它们真正相同,因此不能不安全地使用。为此,它要求源类型和目标类型都允许通过 Typeable 类对其类型进行运行时反射。

【讨论】:

  • 我可以建议在这个答案中添加Data.Coerce.Coercible (coerce) 吗?
  • 对于那些想知道的人,unsafeCoercecoerce 执行相同的操作:实际上什么都没有。他们只改变a -> b的类型,除了coerce是安全的,通过约束它们具有相同的表示Coercible a b => a -> b
【解决方案2】:

我唯一一次不得不使用unsafeCoerce 是在有限自然数上。

{-# 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 是一个单链接数据结构,静态确保小于对其进行参数化的n 类型级别的自然数。

-- 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 可用于安全地索引各种数据结构。它在依赖类型语言中是相当标准的,尽管在 Haskell 中不是。

有时我们希望将Fin n 的值转换为Fin m,其中m 大于n

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

relaxFin 根据定义是无操作的,但仍然需要遍历值才能签出类型。所以我们可能只使用unsafeCoerce 而不是relaxFin。强制包含Fin-s 的更大数据结构可能会导致更显着的速度提升(例如,您可以使用带有Fin-s 作为绑定变量的lambda 项)。

这是一个公认的奇特示例,但我觉得它很有趣,因为它非常安全:我真的想不出外部库或安全用户代码的方法来搞砸这个。不过我可能错了,我很想知道潜在的安全问题。

【讨论】:

    【解决方案3】:

    unsafeCoerce 没用我真的可以推荐,但我可以看到在某些情况下这样的东西可能有用。

    首先想到的是Typeable 相关例程的实现。特别是 cast :: (Typeable a, Typeable b) =&gt; a -&gt; Maybe b 实现了类型安全的行为,因此使用起来很安全,但它在实现中必须玩弄脏把戏。

    也许unsafeCoerce 可以在导入 FFI 子例程以强制类型匹配时找到一些用处。毕竟,FFI 已经允许将不纯的 C 函数作为纯函数导入,因此它本质上是 usafe。请注意,“不安全”并不意味着无法使用,而只是“将举证责任推给程序员”。

    最后,假装sortBy 不存在。那么考虑这个例子:

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

    上面的代码有效,但恕我直言,感觉很愚蠢。我们使用诸如Rev,unRev 之类的函数执行两个maps,我们知道在运行时它们是无操作的。所以我们只是无缘无故地扫描列表两次,只是为了说服编译器使用正确的Ord 实例。

    这些地图对性能的影响应该很小,因为我们也对列表进行了排序。然而,将map Rev 重写为unsafeCoerce :: [Int]-&gt;[Rev] 并节省一些时间是很诱人的。

    注意有强制功能

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

    约束意味着t1t2 的新类型会有所帮助,但会非常危险。考虑

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

    上面会导致数据结构不变量被破坏,因为我们正在改变下面的顺序!由于Data.Set是作为二叉搜索树实现的,所以会造成相当大的破坏。

    【讨论】:

    • 最后一点,IsNewtype 类,在 GHC 7.8 中作为Coercible 实现。为了解决您所说的不变问题,还实现了一个Role Annotation 系统,它允许库编写者定义一个类型是否应该是强制的。这也修复了GeneralizedNewtypeDeriving 中的一个长期存在的错误。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-06
    • 1970-01-01
    • 2015-05-28
    • 1970-01-01
    相关资源
    最近更新 更多