如何证明类型级布尔值的双重否定?

Tob*_*ndt 10 haskell type-level-computation

我有以下模块:

{-# LANGUAGE DataKinds, KindSignatures, TypeFamilies, RoleAnnotations #-}
module Main where

import Data.Coerce (coerce)

-- logical negation for type level booleans
type family Not (x :: Bool) where
    Not True = False
    Not False = True

-- a 3D vector with a phantom parameter that determines whether this is a
-- column or row vector
data Vector (isCol :: Bool) = Vector Double Double Double

type role Vector phantom

-- convert column to row vector or row to column vector
flipVec :: Vector isCol -> Vector (Not isCol)
flipVec = coerce

-- scalar product is only defined for vectors of different types
-- (row times column or column times row vector)
sprod :: Vector isCol -> Vector (Not isCol) -> Double
sprod (Vector x1 y1 z1) (Vector x2 y2 z2) = x1*x2 + y1*y2 + z1*z2

-- vector norm defined in terms of sprod
norm :: Vector isCol -> Double
-- this definition compiles
norm v = sqrt (v `sprod` flipVec v)
-- this does not (without an additional constraint, see below)
norm v = sqrt (flipVec v `sprod` v)

main = undefined
Run Code Online (Sandbox Code Playgroud)

第二个定义norm不编译,因为flipVec v返回Vector (Not isCol)因此需要sprod一个Vector (Not (Not isCol))第二个参数:

Main.hs:22:34:                                                                                                                      
    Couldn't match type ‘isCol’ with ‘Not (Not isCol)’                                                                              
      ‘isCol’ is a rigid type variable bound by                                                                                     
              the type signature for norm :: Vector isCol -> Double                                                                 
              at Main.hs:20:9                                                                                                       
    Expected type: Vector (Not (Not isCol))                                                                                         
      Actual type: Vector isCol                                                                                                     
    Relevant bindings include                                                                                                       
      v :: Vector isCol (bound at Main.hs:22:6)                                                                                     
      norm :: Vector isCol -> Double (bound at Main.hs:22:1)                                                                        
    In the second argument of ‘sprod’, namely ‘v’                                                                                   
    In the first argument of ‘sqrt’, namely ‘(flipVec v `sprod` v)’
Run Code Online (Sandbox Code Playgroud)

我当然可以将约束添加isCol ~ Not (Not isCol)到以下类型norm:

norm :: isCol ~ Not (Not isCol) => Vector isCol -> Double
Run Code Online (Sandbox Code Playgroud)

在调用站点,实际值isCol已知,编译器将看到确实满足此约束.但是实现细节norm泄漏到类型签名中似乎很奇怪.

我的问题:是否有可能以某种方式说服编译器isCol ~ Not (Not isCol)始终为真,因此不需要多余的约束?

use*_*038 5

答案:是的,确实如此.如果您具有正确的数据类型,则证明非常简单:

data family Sing (x :: k) 

class SingI (x :: k) where 
  sing :: Sing x 

data instance Sing (x :: Bool) where 
  STrue :: Sing True 
  SFalse :: Sing False 

type SBool x = Sing (x :: Bool)

data (:~:) x y where 
  Refl :: x :~: x 

double_neg :: SBool x -> x :~: Not (Not x) 
double_neg x = case x of 
                 STrue -> Refl 
                 SFalse -> Refl 
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,编译器将在检查不同情况时看到证明是微不足道的.例如,您可以在多个包中找到所有这些数据定义singletons.您可以像这样使用证明:

instance Sing True where sing = STrue 
instance Sing False where sing = SFalse

norm :: forall isCol . SingI isCol => Vector isCol -> Double
norm v = case double_neg (sing :: Sing isCol) of 
           Refl -> sqrt (flipVec v `sprod` v)
Run Code Online (Sandbox Code Playgroud)

当然这对于这样一件微不足道的事情来说有很多工作要做.如果你确定你知道你在做什么,你可以"作弊":

import Unsafe.Coerce
import Data.Proxy 

double_neg' :: Proxy x -> x :~: Not (Not x) 
double_neg' _ = unsafeCoerce (Refl :: () :~: ())
Run Code Online (Sandbox Code Playgroud)

这允许你摆脱SingI约束:

norm' :: forall isCol . Vector isCol -> Double
norm' v = case double_neg' (Proxy :: Proxy isCol) of 
           Refl -> sqrt (flipVec v `sprod` v)
Run Code Online (Sandbox Code Playgroud)

  • 虽然看起来我们在运行时似乎并不需要字典,但我们无法消除约束(没有作弊),这有点令人不满意.有这个根本原因吗?或者,为了避免这种限制,需要对GHC进行哪些改进? (4认同)
  • @ReidBarton我喜欢GHC那么聪明.可以使用终止检查器,以保证"证据"确实是非底部的.然后,因为`:〜:`只有一个可能的值(让我们相信公理K),GHC可以在运行时完全跳过评估并直接制作/强制`Refl`.由于这一点立即消除,甚至可以优化证据.此外,现在不需要约束,因此在运行时确实不需要传递字典.这可以触发更多优化,例如生成专门化的sans-dictionary. (3认同)
  • 哦,对,整体检查是我正在寻找的主要成分.在运行时评估样张,呃...我想这是'unsafeCoerce`用户在这里招致的证据义务. (2认同)