在运行时检查约束

ram*_*ion 2 haskell

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

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

-- or the more general
claim :: (c => a) -> Maybe a
Run Code Online (Sandbox Code Playgroud)

所以期望的行为是:

>>> :t satisfies @Show show
satisfies @Show show :: a -> Maybe String
>>> satisfies @Show show (0 :: Int)
Just "0"
>>> satisfies @Show show (id :: Int -> Int)
Nothing
Run Code Online (Sandbox Code Playgroud)

目标是在可能的情况下轻松定义充分利用特化的多态函数:

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

作为我可以尝试的最简单的事情,我的第一次尝试尝试使用 -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
Run Code Online (Sandbox Code Playgroud)

这失败了,因为-fdefer-type-errors没有将检查推迟到运行时,或者允许在实际使用的上下文中进行进一步检查(正如我所希望的那样),而是在编译时用等效的替换找到的类型错误error "MESSAGE".

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

Ben*_*son 6

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

也就是说,以下粗略的例子:

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"
Run Code Online (Sandbox Code Playgroud)

生成核心代码,看起来大致如下:

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"
Run Code Online (Sandbox Code Playgroud)

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

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


这样做的一个更常见的方式"是利用专业化的全态函数可能时"将是把功能有问题成型类本身,并给它一个默认的实现(也许一个default签名,如果默认的实现依赖于一些超).那你的showAny样子会是这样的:

{-# LANGUAGE DefaultSignatures #-}
import Data.Typeable

class ShowAny a where
    showAny :: a -> String
    default showAny :: Typeable a => a -> String
    showAny x = "_ :: " ++ show (typeOf x)
Run Code Online (Sandbox Code Playgroud)

你需要为ShowAny你想要使用的所有类型实现showAny,但这通常是一行代码,

instance (Typeable a, Typeable b) => ShowAny (a -> b)
Run Code Online (Sandbox Code Playgroud)

并且您可以通过覆盖来专门化给定类型的实现showAny.

instance ShowAny String where
    showAny = id
Run Code Online (Sandbox Code Playgroud)

您可以在执行通用编程的库中经常看到这种方法.aeson,例如,可以使用GHC.Generics给定类型序列化,并从JSON(所有你需要做的就是派生Generic,写两行instance ToJSON MyType; instance FromJSON MyType),但你也可以写你自己的情况ToJSONFromJSON如果通用代码不够快,或者你需要自定义输出.