Haskell 不同类型的里德尔?

Haskell 不同类型的里德尔?,haskell,higher-kinded-types,data-kinds,Haskell,Higher Kinded Types,Data Kinds,冒着这一风险,是否可能有一个具有不同类型环境的ReaderT?我正在尝试类似于 type AppM (perms :: [*]) = ReaderT (perms :: [*]) IO …但是编译器抱怨 Expected a type, but ‘(perms :: [*])’ has kind ‘[*]’ …大概是因为ReaderT被定义为 newtype ReaderT r (m :: k -> *) (a :: k) = ReaderT {runReaderT :: r ->

冒着这一风险,是否可能有一个具有不同类型环境的
ReaderT
?我正在尝试类似于

type AppM (perms :: [*]) = ReaderT (perms :: [*]) IO
…但是编译器抱怨

Expected a type, but ‘(perms :: [*])’ has kind ‘[*]’
…大概是因为
ReaderT
被定义为

newtype ReaderT r (m :: k -> *) (a :: k) = ReaderT {runReaderT :: r -> m a}
…其中
r
属于
*

我试图在类型级别跟踪权限/角色,我的最终目标是编写如下函数

ensurePermission :: (p :: Permission) -> AppM (p :. ps) ()
。。。其中,对
的每次调用都会在monad的权限列表(在类型级别)中添加/预先添加一个新权限

编辑 我尝试了以下内容,它似乎已经编译好了,但我不确定到底发生了什么。从概念上讲,
perms
不是一种
[*]
。编译器如何接受此代码段,而原始代码段却不是

data HList (l :: [*]) where
  HNil :: HList '[]
  HCons :: e -> HList l -> HList (e ': l)

type AppM (perms :: [*]) = ReaderT (HList perms) IO
编辑#2 我试图改进我的代码片段以进一步匹配我的最终目标,但我又遇到了一个不同的“类”问题:

编译器不接受以下代码:

{-# LANGUAGE GADTs #-}
{-# LANGUAGE DataKinds #-}

data Permission = PermissionA
                | PermissionB

$(genSingletons [''Permission])

data PList (perms :: [Permission]) where
  PNil :: PList '[]
  PCons :: p -> PList perms -> PList (p ': perms)

--     • Expected kind ‘[Permission]’, but ‘p : perms’ has kind ‘[*]’
--     • In the first argument of ‘PList’, namely ‘(p : perms)’
--       In the type ‘PList (p : perms)’
--       In the definition of data constructor ‘PCons’
--    |
-- 26 |   PCons :: p -> PList perms -> PList (p ': perms)
--    |                                       ^^^^^^^^^^

它也不接受以下变化

data PList (perms :: [Permission]) where
  PNil :: PList '[]
  PCons :: (p :: Permission) -> PList perms -> PList (p ': perms)


--     • Expected a type, but ‘(p :: Permission)’ has kind ‘Permission’
--     • In the type ‘(p :: Permission)’
--       In the definition of data constructor ‘PCons’
--       In the data declaration for ‘PList’
--    |
-- 26 |   PCons :: (p :: Permission) -> PList perms -> PList (p ': perms)
--    |            ^^^^^^^^^^^^^^^^^

是的,我想我们在这里有一个XY问题,所以我们后退一步。< /P>
Reader
是一种单子,用于携带方便读取的值。您没有一个值--您有一个要在类型级别强制执行的权限列表--所以我认为您不需要或不需要一个读卡器,或者一个异构列表,或者其他类似的东西

相反,给定一个布尔权限列表:

data Permission = PermissionA | PermissionB deriving (Show)
-- runM with no permissions
runM :: M '[] a -> IO a
runM (M act) = act
data Permission
  = ReadP            -- read content
  | MetaP            -- view (private) metadata
  | WriteP           -- write content
  | AdminP           -- all permissions
  deriving (Show, Eq)
requireRead :: MonadIO m => EntryT' r c m () -> EntryT' r (ReadP:c) m ()
requireRead = unsafeRequire ReadP
requireWrite :: MonadIO m => EntryT' r c m () -> EntryT' r (WriteP:c) m ()
requireWrite = unsafeRequire WriteP
-- plus functions for the rest of the permissions
您希望定义一个在类型级别参数化的monad,并列出其授予的权限。围绕基础
IO
monad的新类型包装器可以:

{-# LANGUAGE DataKinds, KindSignatures, GeneralizedNewtypeDeriving #-}
newtype M (ps :: [Permission]) a = M (IO a) deriving (Functor, Applicative, Monad)
您还需要一个类型函数(也称为类型族)来确定权限是否在权限列表中:

{-# LANGUAGE TypeFamilies, TypeOperators #-}
type family Allowed (p :: Permission) ps where
  Allowed p '[] = False
  Allowed p (p:ps) = True
  Allowed p (q:ps) = Allowed p ps
现在,如果要编写需要特定权限的函数,可以编写以下内容:

deleteA :: (Allowed PermissionA ps ~ True) => M ps ()
deleteA = M $ print "Deleted A"

readB :: (Allowed PermissionB ps ~ True) => M ps ()
readB = M $ print "Read B"

copyBtoA :: ( Allowed PermissionA ps ~ True
            , Allowed PermissionB ps ~ True) => M ps ()
copyBtoA = M $ print "Copied B to A"
要运行
M
操作,我们将引入一个无权限运行的函数:

data Permission = PermissionA | PermissionB deriving (Show)
-- runM with no permissions
runM :: M '[] a -> IO a
runM (M act) = act
data Permission
  = ReadP            -- read content
  | MetaP            -- view (private) metadata
  | WriteP           -- write content
  | AdminP           -- all permissions
  deriving (Show, Eq)
requireRead :: MonadIO m => EntryT' r c m () -> EntryT' r (ReadP:c) m ()
requireRead = unsafeRequire ReadP
requireWrite :: MonadIO m => EntryT' r c m () -> EntryT' r (WriteP:c) m ()
requireWrite = unsafeRequire WriteP
-- plus functions for the rest of the permissions
请注意,如果您尝试
runM readB
,您将得到一个类型错误(无法将
False
True
匹配,这不是最大的错误消息,而是…)

为了授予权限,我们引入了以下功能:

-- grant permissions
grantA :: M (PermissionA:ps) a -> M ps a
grantA (M act) = M act
grantB :: M (PermissionB:ps) a -> M ps a
grantB (M act) = M act
这些函数本质上是术语级的标识函数——它们只是展开并重新包装
M
构造函数。但是,它们在类型级别的操作是向其输入参数添加权限。这意味着:

runM $ grantB $ readB
现在输入检查。我们也要这样做:

runM $ grantA . grantB $ readB
runM $ grantB . grantA $ readB
runM $ grantB . grantA . grantB $ readB
etc.
然后您可以编写如下程序:

program :: IO ()
program = runM $ do
  grantA $ do
    deleteA
    grantB $ do
      readB
      copyBtoA
program1 :: IO ()
program1 = runM $ do
  grantA $ do
    deleteA
    grantB $ do
      readB
    copyBtoA    -- error, needs PermissionB
在拒绝以下程序时:

program :: IO ()
program = runM $ do
  grantA $ do
    deleteA
    grantB $ do
      readB
      copyBtoA
program1 :: IO ()
program1 = runM $ do
  grantA $ do
    deleteA
    grantB $ do
      readB
    copyBtoA    -- error, needs PermissionB
这个基础结构可能有点难看,但它应该是基于类型、完全编译时权限检查所需要的全部

也许可以尝试一下这个版本,看看它是否满足您的需要。完整代码为:

{-# LANGUAGE DataKinds, KindSignatures, GeneralizedNewtypeDeriving,
             TypeFamilies, TypeOperators #-}

data Permission = PermissionA | PermissionB deriving (Show)

newtype M (ps :: [Permission]) a = M (IO a) deriving (Functor, Applicative, Monad)

type family Allowed (p :: Permission) ps where
  Allowed p '[] = False
  Allowed p (p:ps) = True
  Allowed p (q:ps) = Allowed p ps

-- runM with no permissions
runM :: M '[] a -> IO a
runM (M act) = act

-- grant permissions
grantA :: M (PermissionA:ps) a -> M ps a
grantA (M act) = M act
grantB :: M (PermissionB:ps) a -> M ps a
grantB (M act) = M act

deleteA :: (Allowed PermissionA ps ~ True) => M ps ()
deleteA = M $ print "Deleted A"

readB :: (Allowed PermissionB ps ~ True) => M ps ()
readB = M $ print "Read B"

copyBtoA :: ( Allowed PermissionA ps ~ True
            , Allowed PermissionB ps ~ True) => M ps ()
copyBtoA = M $ print "Copied B to A"

program :: IO ()
program = runM $ do
  grantA $ do
    deleteA
    grantB $ do
      readB
      copyBtoA
根据@dfeuer的评论增加了两条注释。首先,它提醒我,
grantA
grantB
同样可以使用
Data中的“safe”
concure
函数编写。concure
如下所示。此版本与上述版本之间生成的代码没有差异,因此这是一个品味问题:

import Data.Coerce

-- grant permissions
grantA :: M (PermissionA:ps) a -> M ps a
grantA = coerce
grantB :: M (PermissionB:ps) a -> M ps a
grantB = coerce
其次,@dfeuer所说的是,在控制权限的受信任代码库和依赖类型系统强制执行权限系统的代码“其余”之间没有明确的区分。例如,
M
构造函数上的模式匹配本质上是危险的,因为您可以从一个权限上下文中提取
IO a
,然后在另一个权限上下文中重建它。(这基本上就是
grantA
grantB
无条件提升特权所做的事情。)如果您在受信任的代码库之外“意外”这样做,您可能最终会绕过权限系统。在许多应用程序中,这并不是什么大问题

但是,如果您试图证明一个系统是安全的,那么您可能需要一个小的可信代码库,它与危险的
M
构造函数一起工作,并且只导出一个“安全”的API,以确保通过类型系统的安全性。在这种情况下,您将有一个模块导出类型
M
,而不是其构造函数
M(…)
。相反,您应该导出智能构造函数来创建具有适当权限的
M
操作

此外,出于模糊的技术原因,即使不导出
M
构造函数,“不受信任”的代码仍有可能在不同的权限上下文之间进行强制:

stealPermission :: M (PermissionA:ps) a -> M ps a
stealPermission = coerce
因为
M
类型构造函数的第一个参数有一个所谓的“role”,默认为“phantom”而不是“nominal”。如果覆盖此选项:

{-# LANGUAGE RoleAnnotations #-}
type role M nominal _

然后,
强制
只能在构造函数在作用域内的情况下使用,这就堵塞了这个漏洞。不受信任的代码仍然可以使用
非安全性
,但有一些机制(谷歌的“安全Haskell”)可以防止这种情况发生。

在另一个要点中,您评论道:


@K.A.Buhr,哇!谢谢你如此详细的答复。你是对的,这是一个XY问题,你已经基本解决了我试图解决的实际问题。上下文的另一个重要部分是,在某种程度上,这些类型级别的权限必须在值级别“具体化”。这是因为最终检查是针对授予当前登录用户的权限进行的,这些权限存储在数据库中

考虑到这一点,我计划有两个“通用”功能,比如:

requiredPermission :: (RequiredPermission p ps) => Proxy p -> AppM ps ()
optionalPermission :: (OptionalPermission p ps) => Proxy p -> AppM ps ()
区别在于:

  • requiredPermission
    只需将权限添加到类型级别列表中,并在调用
    runAppM
    时对其进行验证。如果当前用户没有所有必需的权限,则
    runAppM
    将立即向UI抛出401错误
  • 另一方面,
    optionalPermission
    将从
    阅读器
    环境中提取用户,ch
    readPageWithMeta :: ( Allowed 'ReadP perms ~ 'True, Allowed 'MetaP perms ~ 'True
        , MonadIO m) => Int -> AppT perms m ()
    readPageWithMeta n = do
      readPage n
      metaPage n
    
    readPageWithOptionalMeta :: ( Allowed 'ReadP perms ~ 'True
        , MonadIO m) => Int -> AppT perms m ()
    readPageWithOptionalMeta n = do
      readPage n
      whenMeta $ metaPage n
    
    newtype EntryT' (reqP :: [Permission]) (checkedP :: [Permission]) m a
      = EntryT (ReaderT Env m a)
      deriving (Functor, Applicative, Monad, MonadReader Env, MonadIO)
    type EntryT reqP = EntryT' reqP reqP
    
    runEntryT :: MonadIO m => User -> EntryT req m () -> m ()
    runEntryT u (EntryT act)
      = case lookup u userDB of
          Nothing   -> say $ "error 401: no such user '" ++ u ++ "'"
          Just perms -> runReaderT act (Env perms u)
    
    entryReadPage :: MonadIO m => Int -> EntryT '[ReadP] m ()
    entryReadPage n = _somethingspecial_ $ do
      readPage n
      whenMeta $ metaPage n
    
    (Allowed 'ReadP perms ~ 'True, MonadIO m) => Int -> AppT perms m ()
    
    requireRead :: MonadIO m => EntryT' r c m () -> EntryT' r (ReadP:c) m ()
    requireRead = unsafeRequire ReadP
    requireWrite :: MonadIO m => EntryT' r c m () -> EntryT' r (WriteP:c) m ()
    requireWrite = unsafeRequire WriteP
    -- plus functions for the rest of the permissions
    
    unsafeRequire :: MonadIO m => Permission -> EntryT' r c m () -> EntryT' r c' m ()
    unsafeRequire p act = do
      ps <- asks uperms
      if allowed p ps
        then coerce act
        else say $ "error 403: requires permission " ++ show p
    
    entryReadPage :: MonadIO m => Int -> EntryT '[ReadP] m ()
    entryReadPage n = requireRead . _ $ do
      readPage n
      whenMeta $ metaPage n
    
    AppT perms0 m0 () -> EntryT' '[ReadP] '[] m ()
    
    toRunAppT :: MonadIO m => AppT r m a -> EntryT' r '[] m a
    toRunAppT = coerce
    
    entryReadPage :: MonadIO m => Int -> EntryT '[ReadP] m ()
    entryReadPage n = requireRead . toRunAppT $ do
      readPage n
      whenMeta $ metaPage n
    
    entryEditPage :: MonadIO m => Int -> EntryT '[ReadP, WriteP] m ()
    entryEditPage n = requireRead . requireWrite . toRunAppT $ do
      editPage n
      whenMeta $ metaPage n
    
    whenMeta :: Monad m => AppT (MetaP:perms) m () -> AppT perms m ()
    whenMeta = unsafeWhen MetaP
    -- and similar functions for other permissions
    
    unsafeWhen :: Monad m => Permission -> AppT perms m () -> AppT perms' m ()
    unsafeWhen p act = do
      ps <- asks uperms
      if allowed p ps
        then coerce act
        else return ()
    
    Username/Req (e.g., "alice Read 5"): alice Read 5    -- Alice...
    Read page 5
    Username/Req (e.g., "alice Read 5"): bob Read 5      -- and Bob can read.
    Read page 5
    Username/Req (e.g., "alice Read 5"): carl Read 5     -- Carl gets the metadata, too
    Read page 5
    Secret metadata 25
    Username/Req (e.g., "alice Read 5"): bob Edit 3      -- Bob can't edit...
    error 403: requires permission WriteP
    Username/Req (e.g., "alice Read 5"): alice Edit 3    -- but Alice can.
    Edit page 3
    Username/Req (e.g., "alice Read 5"):
    
    {-# LANGUAGE DataKinds #-}
    {-# LANGUAGE KindSignatures #-}
    {-# LANGUAGE GeneralizedNewtypeDeriving #-}
    {-# LANGUAGE TypeFamilies #-}
    {-# LANGUAGE TypeOperators #-}
    
    module Realistic where
    
    import Control.Monad.Reader
    import Data.Coerce
    
    -- |Set of permissions
    data Permission
      = ReadP            -- read content
      | MetaP            -- view (private) metadata
      | WriteP           -- write content
      | AdminP           -- all permissions
      deriving (Show, Eq)
    
    type User = String
    -- |User database
    userDB :: [(User, [Permission])]
    userDB
      = [ ("alice", [ReadP, WriteP])
        , ("bob",   [ReadP])
        , ("carl",  [AdminP])
        ]
    
    -- |Environment with 'uperms' and whatever else is needed
    data Env = Env
      { uperms :: [Permission]   -- user's actual permissions
      , user :: String           -- other Env stuff
      } deriving (Show)
    
    -- |Check for permission in type-level and term-level lists
    type family Allowed (p :: Permission) ps where
      Allowed p (AdminP:ps) = True   -- admins can do anything
      Allowed p '[] = False
      Allowed p (p:ps) = True
      Allowed p (q:ps) = Allowed p ps
    allowed :: Permission -> [Permission] -> Bool
    allowed p (AdminP:ps) = True
    allowed p (q:ps) | p == q = True
                     | otherwise = allowed p ps
    allowed p [] = False
    
    -- |An application action running with a given list of checked permissions.
    newtype AppT (perms :: [Permission]) m a = AppT (ReaderT Env m a)
      deriving (Functor, Applicative, Monad, MonadReader Env, MonadIO)
    
    -- Optional actions run if permissions are available at runtime.
    whenRead :: Monad m => AppT (ReadP:perms) m () -> AppT perms m ()
    whenRead = unsafeWhen ReadP
    whenMeta :: Monad m => AppT (MetaP:perms) m () -> AppT perms m ()
    whenMeta = unsafeWhen MetaP
    whenWrite :: Monad m => AppT (WriteP:perms) m () -> AppT perms m ()
    whenWrite = unsafeWhen WriteP
    whenAdmin :: Monad m => AppT (AdminP:perms) m () -> AppT perms m ()
    whenAdmin = unsafeWhen AdminP
    unsafeWhen :: Monad m => Permission -> AppT perms m () -> AppT perms' m ()
    unsafeWhen p act = do
      ps <- asks uperms
      if allowed p ps
        then coerce act
        else return ()
    
    -- |An entry point, requiring a list of permissions
    newtype EntryT' (reqP :: [Permission]) (checkedP :: [Permission]) m a
      = EntryT (ReaderT Env m a)
      deriving (Functor, Applicative, Monad, MonadReader Env, MonadIO)
    -- |An entry point whose full list of required permission has been (statically) checked).
    type EntryT reqP = EntryT' reqP reqP
    
    -- |Run an entry point whose required permissions have been checked.
    runEntryT :: MonadIO m => User -> EntryT req m () -> m ()
    runEntryT u (EntryT act)
      = case lookup u userDB of
          Nothing   -> say $ "error 401: no such user '" ++ u ++ "'"
          Just perms -> runReaderT act (Env perms u)
    
    -- Functions to build the list of required permissions for an entry point.
    requireRead :: MonadIO m => EntryT' r c m () -> EntryT' r (ReadP:c) m ()
    requireRead = unsafeRequire ReadP
    requireMeta :: MonadIO m => EntryT' r c m () -> EntryT' r (MetaP:c) m ()
    requireMeta = unsafeRequire MetaP
    requireWrite :: MonadIO m => EntryT' r c m () -> EntryT' r (WriteP:c) m ()
    requireWrite = unsafeRequire WriteP
    requireAdmin :: MonadIO m => EntryT' r c m () -> EntryT' r (AdminP:c) m ()
    requireAdmin = unsafeRequire AdminP
    unsafeRequire :: MonadIO m => Permission -> EntryT' r c m () -> EntryT' r c' m ()
    unsafeRequire p act = do
      ps <- asks uperms
      if allowed p ps
        then coerce act
        else say $ "error 403: requires permission " ++ show p
    
    -- Adapt an entry point w/ all static checks to an underlying application action.
    toRunAppT :: MonadIO m => AppT r m a -> EntryT' r '[] m a
    toRunAppT = coerce
    
    -- Example application actions
    readPage :: (Allowed ReadP perms ~ True, MonadIO m) => Int -> AppT perms m ()
    readPage n = say $ "Read page " ++ show n
    metaPage :: (Allowed ReadP perms ~ True, MonadIO m) => Int -> AppT perms m ()
    metaPage n = say $ "Secret metadata " ++ show (n^2)
    editPage :: (Allowed ReadP perms ~ True, Allowed WriteP perms ~ True, MonadIO m) => Int -> AppT perms m ()
    editPage n = say $ "Edit page " ++ show n
    
    say :: MonadIO m => String -> m ()
    say = liftIO . putStrLn
    
    -- Example entry points
    entryReadPage :: MonadIO m => Int -> EntryT '[ReadP] m ()
    entryReadPage n = requireRead . toRunAppT $ do
      readPage n
      whenMeta $ metaPage n
    entryEditPage :: MonadIO m => Int -> EntryT '[ReadP, WriteP] m ()
    entryEditPage n = requireRead . requireWrite . toRunAppT $ do
      editPage n
      whenMeta $ metaPage n
    
    -- Test harnass
    data Req = Read Int
             | Edit Int
             deriving (Read)
    main :: IO ()
    main = do
      putStr "Username/Req (e.g., \"alice Read 5\"): "
      ln <- getLine
      case break (==' ') ln of
        (user, ' ':rest) -> case read rest of
          Read n -> runEntryT user $ entryReadPage n
          Edit n -> runEntryT user $ entryEditPage n
      main