在运行时检查约束

时间:2017-06-06 19:22:00

标签: haskell

我正在尝试定义一个函数来检测输入的类型是否满足给定的约束:

satisfies :: (c a => a -> b) -> a -> Maybe b

-- or the more general
claim :: (c => a) -> Maybe a

所以期望的行为是:

>>> :t satisfies @Show show
satisfies @Show show :: a -> Maybe String
>>> satisfies @Show show (0 :: Int)
Just "0"
>>> satisfies @Show show (id :: Int -> Int)
Nothing

目标是使定义完全多态函数变得容易 尽可能优化专业化:

showAny :: a -> String
showAny (satisfies @Show show -> Just str) = str
showAny (satisfies @Typeable showType -> Just str) = "_ :: " ++ str
showAny _ = "_"

作为我可以尝试的最简单的事情,我的第一次尝试尝试使用-fdefer-to-runtime

{-# OPTIONS_GHC -fdefer-type-errors -Wno-deferred-type-errors #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE RankNTypes #-}
module Claim where
import System.IO.Unsafe (unsafePerformIO)
import System.IO.Error (catchIOError)

satisfies :: (c a => a -> b) -> a -> Maybe b
satisfies f a = unsafePerformIO $
  (return . Just $! f a) `catchIOError` \_ -> return Nothing

此操作失败,因为-fdefer-type-errors不会将检查推迟到 运行时,或允许在上下文中进一步检查 实际使用(正如我所希望的那样),但在编译时替换了找到的 类型错误,相当于error "MESSAGE"

现在我没有想法。是否可以实施satisfies

2 个答案:

答案 0 :(得分:6)

您无法在运行时调度实例可用性。请记住,编译器将约束转换为类型类字典 - 显式传递并在运行时显式访问的函数记录。 "胖箭" =>在运行时由一个"细箭头"表示。 ->,因此elaborator需要在编译时知道要传递哪个字典。

即,以下原始示例:

class Show a where
    show :: a -> String

instance Show String where
    show = id

showTwice :: Show a => a -> String
showTwice x = show x ++ show x

main = putStrLn $ showTwice "foo"

生成类似于:

的核心代码
data Show_ a = Show_ { show :: a -> String }

showString_ :: Show_ String
showString_ = Show_ { show = id }

showTwice :: Show_ a -> a -> String
showTwice show_ x = show show_ x ++ show show_ x

main = putStrLn $ showTwice showString_ "foo"

main生成代码时,编译器需要知道在哪里找showString_

您可以想象一个系统,您可以在运行时使用某种内省机制查找类型类字典,但从语言设计的角度来看,这会产生奇怪的行为。问题是孤儿实例。如果我编写一个试图在模块A中查找给定实例的函数,并在不相关的模块B中定义这样的实例,那么从某个客户端模块调用该函数时的行为{{ 1}}取决于C是否由程序的其他部分导入。太奇怪了!

更常用的做法"完全多态函数,在可能的情况下利用特化"将问题中的函数放入类型类本身并给它一个默认实现(如果默认实现依赖于某个超类,可能使用a default signature)。您的B将如下所示:

showAny

您需要为要使用{-# LANGUAGE DefaultSignatures #-} import Data.Typeable class ShowAny a where showAny :: a -> String default showAny :: Typeable a => a -> String showAny x = "_ :: " ++ show (typeOf x) 的所有类型实施ShowAny,但这通常只需一行代码,

showAny

并且您可以通过覆盖instance (Typeable a, Typeable b) => ShowAny (a -> b) 来专门化给定类型的实现。

showAny

在执行通用编程的库中,您经常会看到这种方法。例如,aeson可以使用instance ShowAny String where showAny = id 将给定类型与JSON串行化(您所要做的就是派生GHC.Generics并写两行Generic),但是如果通用代码不够快或您需要自定义输出,您也可以编写自己的instance ToJSON MyType; instance FromJSON MyTypeToJSON实例。

答案 1 :(得分:0)

the accepted answer的替代解决方法是手动传递字典。

即,给定:

{-# LANGUAGE GADTs #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeOperators #-}
module Claim where

data Proof c where QED :: c => Proof c
type Claim c = Maybe (Proof c)
type c ? a = Maybe (Proof (c a))

可以写:

showAny :: (Show? a, Typeable? a) -> a -> String
showAny (Just QED, _) a = show a
showAny (_, Just QED) a = "_ :: " ++ showType a
showAny _ _ = "_"

哪种方法效果很好:

>>> showAny (Nothing, Just QED) (id :: Int -> Int)
"_ :: Int -> Int"
>>> showAny (Just QED, Just QED) (0 :: Int)
"0"
>>> showAny (Nothing, Nothing) undefined
"_"