Разновидность ReaderT?

Если это может превратиться в XY-проблему, возможно ли иметь 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) ()

... где каждый вызов ensurePermission добавляет/добавляет новое разрешение в список разрешений монады (на уровне типа).

Редактировать

Я попробовал следующее, и кажется, что оно компилируется, но я не уверен, что происходит. Концептуально не 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)
--    |            ^^^^^^^^^^^^^^^^^

person Saurabh Nanda    schedule 29.06.2019    source источник
comment
Часть проблемы заключается в том, что вид [*] не имеет типов со значениями. Из-за этого средство проверки типов отклонит ваш ensurePermission. Вы можете иметь типы вида * (или #) только в тех местах, где у вас есть значения, если это имеет смысл. Вам нужно что-то вроде Proxy.   -  person David Young    schedule 29.06.2019
comment
Вы уверены, что не хотите вместо этого type AppM (perms :: [*]) = ReaderT (Hlist perms) IO? То есть читатель получает доступ к значению для любого типа в списке типов?   -  person chi    schedule 29.06.2019
comment
@ Дэвид, я не уверен, что полностью тебя понимаю. Тем более, что я черпаю вдохновение в типе Context и классе типов HasContextEntry Servant - stackage.org/haddock/lts-12.1/servant-server-0.14.1/   -  person Saurabh Nanda    schedule 29.06.2019
comment
@чи, может быть, ты и прав. Я предполагаю, что HList — это неоднородный список, верно? Какая (хорошо поддерживаемая) библиотека обеспечивает это? Я попытался поискать в stackage/hackage и нашел их много. На самом деле сам Servant предоставляет тип HList, но только для HTTP-заголовков.   -  person Saurabh Nanda    schedule 29.06.2019
comment
@chi я попробовал ваше предложение, и, кажется, оно компилируется. Но я не уверен, почему. Я отредактировал вопрос.   -  person Saurabh Nanda    schedule 29.06.2019
comment
Да, это неоднородный список. Меня также смутило множество пакетов, предоставляющих его — я не знаю, какой из них будет правильным использовать.   -  person chi    schedule 29.06.2019
comment
Таким образом, perms по-прежнему имеет тип [*] (список типов), но HList :: [*] -> * преобразует его в простой тип, чтобы его можно было использовать в монаде чтения. Тип perms не имеет значения, имеет значение только тип аргумента ReaderT.   -  person chi    schedule 29.06.2019
comment
@chi Я застрял с новой проблемой. Я попытался специализировать HList, переименовав его в PList (perms :: [Permission]), но компилятору не понравилось то, что я сделал. Я снова редактирую вопрос.   -  person Saurabh Nanda    schedule 29.06.2019
comment
@SaurabhNanda Типы, типы которых поднимаются с помощью -XDataKinds, не имеют значений, они представляют собой чисто метаданные уровня типа. Типы типа Type (ранее известные как *) имеют значения. p имеет тип Permission, а не Type, и поэтому не может иметь значений. Вы можете попробовать передать Proxy, параметризованный p. У прокси есть параметр фантомного типа, который является многородным. Вы также можете попробовать использовать -XTypeApplications для указания p вместо фактического значения.   -  person danidiaz    schedule 29.06.2019
comment
@danidiaz Есть ли способ провести самоанализ (используя что-то вроде :t, :i и т. д.) и выяснить, какие типы имеют значения, а какие нет? Кроме того, я не понимаю, зачем компилятору беспокоиться о типах, которые могут иметь значения, и типах, которые не могут. Я попробовал следующее - data EmptyType ; foo :: EmptyType -> () ; foo _ = () и он скомпилировался. Это происходит из-за ':? Ожидается ли, что оба операнда будут типа * ? Как проверить оператор ': в GHCi?   -  person Saurabh Nanda    schedule 29.06.2019
comment
Я совсем запутался в законах/правилах работы с разными видами. Могу ли я не писать конструкторы данных, используя типы разных типов (т.е. не *)? Или в том, что оператор уровня типа ': относится к типу * -> [*] -> * и поэтому не может работать с типами другого типа.   -  person Saurabh Nanda    schedule 29.06.2019
comment
@ Дэвид, похоже, я столкнулся с проблемой, о которой вы говорили в своем комментарии. Если компилятор проверяет, какие типы могут иметь значения, а какие нет, я не понимаю, почему компилируется следующее - data EmptyType ; foo :: EmptyType -> () ; foo _ = ()   -  person Saurabh Nanda    schedule 29.06.2019
comment
аааа... почему компилируется следующее! Где документировано все это поведение? data PList (perms :: [Permission]) where PCons :: Proxy (p :: Permission) -> PList perms -> PList (p ': perms)   -  person Saurabh Nanda    schedule 29.06.2019
comment
Пример с Proxy компилируется, потому что тип Proxy имеет конструктор значений, также названный Proxy. Однако значение не используется, Proxy служит только обходным путем для передачи информации на уровне типа. Обратите внимание, что тип конструктора типа Proxy похож на forall {k}. k -> Type. Вы даете ему параметр (фантомного) типа любого типа, и он дает вам тип вида Type (также известный как *), который может иметь значения, а именно значение Proxy.   -  person danidiaz    schedule 29.06.2019
comment
@SaurabhNanda Я плохо сформулировал это (что я понял слишком долго после того, как оставил комментарий). Это две разные вещи: 1) Тип, такой как [*], не имеет типов, которые содержат значения. 2) Компилятор не позволит типам вида [*] (и подобным) встречаться в позициях значений (на самом деле не уверен, какая точная фраза будет здесь, поэтому дайте мне знать, если это не очень ясно). 2 не полностью из-за 1, но они оба (по отдельности) верны.   -  person David Young    schedule 29.06.2019
comment
Кроме того, вы можете возразить, что ваш пустой тип действительно имеет значения: нижнее значение. А поскольку типы вида [*] даже не будут компилироваться в такой позиции, они на самом деле не имеют никаких значений. Но на самом деле это не совсем то, что проверяет компилятор, когда он отклоняет тип, подобный f :: ['a','b','c'] -> ().   -  person David Young    schedule 29.06.2019
comment
@chi, vinyl в хорошем состоянии, а HList в основном Rec Identity.   -  person dfeuer    schedule 29.06.2019


Ответы (2)


В отдельном Gist вы прокомментировали:

@K.A.Buhr, вау! Спасибо за такой подробный ответ. Вы правы в том, что это проблема XY, и вы в значительной степени решили реальную проблему, которую я пытаюсь решить. Другая важная часть контекста заключается в том, что в какой-то момент эти разрешения на уровне типов должны быть подтверждены на уровне значений. Это связано с тем, что окончательная проверка выполняется против разрешений, предоставленных текущему пользователю, вошедшему в систему, которые хранятся в БД.

Принимая это во внимание, я планирую иметь две общие функции, скажем:

requiredPermission :: (RequiredPermission p ps) => Proxy p -> AppM ps ()
optionalPermission :: (OptionalPermission p ps) => Proxy p -> AppM ps ()

Вот разница:

  • requiredPermission просто добавит разрешение в список на уровне типов, и оно будет проверено при вызове runAppM. Если у текущего пользователя нет ВСЕХ необходимых разрешений, то runAppM немедленно выдаст ошибку 401 в пользовательский интерфейс.
  • С другой стороны, optionalPermission извлечет пользователя из среды Reader, проверит разрешение и вернет True/False. runAppM ничего не будет делать с OptionalPermissions. Это будет для случаев, когда отсутствие разрешения НЕ должно привести к сбою всего действия, но пропустить определенный шаг в действии.

Учитывая этот контекст, я не уверен, закончу ли я функциями, такими как GrantA или GrantB. Развертывание ВСЕХ RequestPermissions в конструкторе AppM будет выполнено с помощью runAppM, что также гарантирует, что текущий пользователь, вошедший в систему, действительно имеет эти разрешения.

Обратите внимание, что существует более одного способа материализовать типы. Например, следующая программа — с помощью коварной черной магии — умудряется материализовать тип времени выполнения без использования прокси или синглетонов!

main = do
  putStr "Enter \"Int\" or \"String\": "
  s <- getLine
  putStrLn $ case s of "Int" ->    "Here is an integer: " ++ show (42 :: Int)
                       "String" -> "Here is a string: " ++ show ("hello" :: String)

Точно так же следующий вариант grantA позволяет поднять пользовательские разрешения, известные только во время выполнения, на уровень типа:

whenA :: M (PermissionA:ps) () -> M ps ()
whenA act = do
  perms <- asks userPermissions  -- get perms from environment
  if PermissionA `elem` perms
    then act
    else notAuthenticated

Здесь можно использовать синглтоны, чтобы избежать шаблонов для разных разрешений и улучшить безопасность типов в этом доверенном фрагменте кода (т. Е. Чтобы два вхождения PermissionA принудительно совпадали). Точно так же типы ограничений могут экономить 5 или 6 символов на проверку разрешений. Однако ни одно из этих улучшений не является необходимым, и они могут добавить существенную сложность, которой следует избегать, если это вообще возможно, пока после вы не получите работающий прототип. Другими словами, элегантный код, который не работает, не так уж элегантен.

В этом духе, вот как я мог бы адаптировать свое исходное решение для поддержки набора необходимых разрешений, которые должны быть удовлетворены в определенных точках входа (например, определенные маршрутизируемые веб-запросы), и для выполнения проверки разрешений во время выполнения по пользовательской базе данных.

Во-первых, у нас есть набор разрешений:

data Permission
  = ReadP            -- read content
  | MetaP            -- view (private) metadata
  | WriteP           -- write content
  | AdminP           -- all permissions
  deriving (Show, Eq)

и пользовательские базы данных:

type User = String
userDB :: [(User, [Permission])]
userDB
  = [ ("alice", [ReadP, WriteP])
    , ("bob",   [ReadP])
    , ("carl",  [AdminP])
    ]

а также среду, которая включает в себя разрешения пользователей и все остальное, что вы хотите носить с собой в ридере:

data Env = Env
  { uperms :: [Permission]   -- user's actual permissions
  , user :: String           -- other Env stuff
  } deriving (Show)

Мы также хотим, чтобы функции на уровне типа и термина проверяли списки разрешений:

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

(Да, вы можете использовать библиотеку singletons для одновременного определения обеих функций, но пока давайте сделаем это без синглетонов.)

Как и прежде, у нас будет монада со списком разрешений. Вы можете думать об этом как о списке разрешений, которые были проверены и подтверждены на данном этапе кода. Мы сделаем это преобразователем монады для общего m с компонентом ReaderT Env:

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
newtype AppT (perms :: [Permission]) m a = AppT (ReaderT Env m a)
  deriving (Functor, Applicative, Monad, MonadReader Env, MonadIO)

Теперь мы можем определить действия в этой монаде, которые образуют строительные блоки для нашего приложения:

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

В каждом случае действие разрешено в любом контексте, где список разрешений, которые были проверены и проверены, включает необходимые разрешения, перечисленные в сигнатуре типа. (Да, виды ограничений здесь будут работать нормально, но давайте не будем усложнять.)

Мы можем построить из них более сложные действия, как мы сделали в моем другом ответе:

readPageWithMeta :: ( Allowed 'ReadP perms ~ 'True, Allowed 'MetaP perms ~ 'True
    , MonadIO m) => Int -> AppT perms m ()
readPageWithMeta n = do
  readPage n
  metaPage n

Обратите внимание, что GHC может фактически автоматически определить этот тип подписи, определяя, что требуются разрешения ReadP и MetaP. Если бы мы хотели сделать разрешение MetaP необязательным, мы могли бы написать:

readPageWithOptionalMeta :: ( Allowed 'ReadP perms ~ 'True
    , MonadIO m) => Int -> AppT perms m ()
readPageWithOptionalMeta n = do
  readPage n
  whenMeta $ metaPage n

где whenMeta разрешает необязательное действие в зависимости от доступных разрешений. (См. ниже.) Опять же, эта подпись может быть определена автоматически.

До сих пор, хотя мы разрешали необязательные разрешения, мы явно не имели дело с обязательными разрешениями. Они будут указаны в точках входа, которые будут определены с использованием отдельной монады:

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

Это требует некоторого пояснения. EntryT' (с галочкой) имеет два списка разрешений. Первый представляет собой полный список необходимых разрешений для точки входа и имеет фиксированное значение для каждой конкретной точки входа. Второй — это подмножество тех разрешений, которые были проверены (в статическом смысле, когда имеется вызов функции для проверки и проверки наличия у пользователя требуемых разрешений). Он будет построен из пустого списка в полный список необходимых разрешений, когда мы определим точки входа. Мы будем использовать его в качестве механизма на уровне типов, чтобы гарантировать наличие правильного набора вызовов функций проверки разрешений. EntryT (без галочки) имеет свои (статически) проверенные разрешения, равные требуемым разрешениям, и именно поэтому мы знаем, что его безопасно запускать (против динамически определяемого набора разрешений конкретного пользователя, которые все будут проверяться как гарантированные типом) .

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

Обратите внимание, что здесь у нас есть блок do, построенный из строительных блоков AppT. На самом деле он эквивалентен readPageWithOptionalMeta выше и поэтому имеет тип:

(Allowed 'ReadP perms ~ 'True, MonadIO m) => Int -> AppT perms m ()

_somethingspecial_ здесь необходимо адаптировать этот AppT (чей список разрешений требует, чтобы ReadP был проверен и подтвержден перед его запуском) к точке входа, чьи списки необходимых и (статически) проверяемых разрешений - [ReadP]. Мы сделаем это, используя набор функций для проверки фактических разрешений во время выполнения:

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

внешний тип правильный, что отражает тот факт, что список requireXXX функций соответствует списку необходимых разрешений в сигнатуре типа. Оставшееся отверстие имеет тип:

AppT perms0 m0 () -> EntryT' '[ReadP] '[] m ()

Из-за того, как мы структурировали нашу проверку разрешений, это особый случай безопасного преобразования:

toRunAppT :: MonadIO m => AppT r m a -> EntryT' r '[] m a
toRunAppT = coerce

Другими словами, мы можем написать окончательное определение точки входа, используя довольно приятный синтаксис, который буквально говорит, что нам требуется Read для запуска этого AppT:

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

Обратите внимание, что список требуемых разрешений явно включен в тип точки входа, а составленный список requireXXX функций, выполняющих проверку этих разрешений во время выполнения, должен точно соответствовать тем же самым разрешениям в том же порядке, чтобы он выполнял проверку типов.

Последняя часть головоломки — это реализация whenMeta, которая выполняет проверку разрешений во время выполнения и выполняет необязательное действие, если разрешение доступно.

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
person K. A. Buhr    schedule 30.06.2019
comment
Еще раз спасибо за мини-пост в блоге :) Я попытаюсь поиграть с вашими примерами кода, когда буду на своем ноутбуке, но первое, что бросается в глаза, это необходимость явного преобразования (Allowed p perms) => AppT pems m в более конкретную форму, используя механическое requireX . toRunAppT преобразование. Нет ли способа использовать ограничения на AppT perms m для автоматической конкретизации монады? Список на уровне типов, который строит requiredX, не может быть построен при вызове readPage / writePage. Кстати, именно так я попал в кроличью нору оригинального ReaderT/HList. - person Saurabh Nanda; 01.07.2019
comment
Да, это возможно. Я намеренно избегал этого, потому что это плохой дизайн. Вы думаете, что хотите, чтобы необходимые разрешения доходили до точек входа, а затем проверялись каким-то непрозрачным образом на верхнем уровне, но затем (1) вы упрощаете внесение изменений в код внутри приложения блокировать пользователей от точек входа во время выполнения; и (2) вы теряете явную спецификацию безопасности приложения, предоставленную сигнатурами точки входа. - person K. A. Buhr; 01.07.2019
comment
Я провел много времени, возясь с кодом, чтобы понять недостатки наличия общей/единственной функции для проверки разрешений, но не смог этого понять. Не могли бы вы пояснить свой комментарий: (1) you make it easier for code changes in the guts of the app to lock users out of entry points at runtime; and (2) you lose the explicit app security specification provided by the entry point signatures. - person Saurabh Nanda; 02.07.2019
comment
(1) Если вы реорганизуете низкоуровневый код, используемый несколькими точками входа, легко случайно ввести новое требование разрешения. Если разрешения просачиваются вверх, ошибки типа нет, но внезапное посещение домашней страницы требует CommentEditP. (2) Что касается (1), сигнатуры типа точки входа предоставляют документированную спецификацию гарантий безопасности вашего приложения. Какие разрешения необходимы для запуска entryX? Проверьте подпись. Изменения кода требуют изменений спецификации? За этими изменениями должен следить человек, а не компилятор. - person K. A. Buhr; 02.07.2019
comment
Спасибо за разъяснение - теперь это имеет смысл для меня. Кстати, я воспользовался советами из вашего решения и попробовал PoC для своего приложения по адресу gist.github.com/saurabhnanda/ — сейчас это для определенных флагов функций, которые включены для каждого клиента, а не для разрешений. У меня есть полиморфная функция requireFeature, которая может ввести ограничение на уровне типа для любого переданного ей флага функции. Точно так же существует полиморфный runAction, который требует от вас передачи списка флагов функций в качестве прокси (продолжение..) - person Saurabh Nanda; 03.07.2019
comment
(продолжение.) Поскольку runAction приходится вызывать с явным списком флагов функций, это защищает программистов от рефакторинга и искажения флагов функций (или, в будущем, списков разрешений). . Предвидите ли вы какие-либо проблемы с этим упрощенным подходом? - person Saurabh Nanda; 03.07.2019

Да, я думаю, что у нас здесь проблема XY, так что давайте сделаем шаг назад.

Reader – это монада для переноса значения, которое удобно читать. У вас нет значения — у вас есть список разрешений, которые вы хотите применить на уровне типа — так что я не думаю, что вам нужен или нужен читатель, разнородный список или что-то в этом роде.

Вместо этого, учитывая список логических разрешений:

data Permission = PermissionA | PermissionB deriving (Show)

вы хотите определить монаду, параметризованную на уровне типа со списком предоставленных разрешений. Обертка newtype вокруг вашей основной монады IO подойдет:

{-# LANGUAGE DataKinds, KindSignatures, GeneralizedNewtypeDeriving #-}
newtype M (ps :: [Permission]) a = M (IO a) deriving (Functor, Applicative, Monad)

Вам также понадобится функция типа (семейство типов AKA), чтобы определить, находится ли разрешение в списке разрешений:

{-# 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, мы вводим функцию, которая запускает его без разрешений:

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

Обратите внимание, что если вы попробуете 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

Эта инфраструктура может быть немного уродливой, но она должна быть всем, что вам нужно для проверки разрешений на основе типов и во время компиляции.

Может быть, попробовать эту версию немного и посмотреть, соответствует ли она вашим потребностям. Полный код:

{-# 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 можно с одинаковым успехом записать с помощью "безопасной" функции coerce из Data.Coerce следующим образом. Нет никакой разницы в коде, сгенерированном между этой версией и версией выше, так что это дело вкуса:

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 имеет так называемую «роль», которая по умолчанию является «фантомной», а не «номинальной». Если вы переопределите это:

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

тогда coerce можно использовать только там, где конструктор находится в области видимости, что закрывает эту лазейку. Ненадежный код все еще может использовать unsafeCoerce, но есть механизмы (Google для «Safe Haskell»), чтобы предотвратить это.

person K. A. Buhr    schedule 29.06.2019
comment
Должна быть аннотация роли для защиты фантома, а конструктор нового типа не должен экспортироваться. - person dfeuer; 30.06.2019
comment
Хорошо, я добавил примечание в конце, которое, я думаю, касается этого. - person K. A. Buhr; 30.06.2019
comment
@K.A.Buhr, вау! Спасибо за такой подробный ответ. Вы правы в том, что это проблема XY, и вы в значительной степени решили реальную проблему, которую я пытаюсь решить. В итоге я написал очень длинный ответ, который превысил лимит комментариев StackOverflow. Посмотрите, можете ли вы читать gist.github.com/saurabhnanda/b783c4a99d56c527613cf6cb3febce4c. - person Saurabh Nanda; 30.06.2019
comment
@K.A.Buhr, мне нужно включить AllowAmbiguousTypes, чтобы иметь возможность писать такие функции, как readA и deleteB. Я попробовал что-то вроде gist.github.com/saurabhnanda/ и компилятор получает неоднозначные переменные типа. - person Saurabh Nanda; 30.06.2019
comment
@ K.A.Buhr, есть ли способ заставить эту технику работать с полностью полиморфной монадой m? Для возможности тестирования весь наш код полиморфен в m и конкретизируется только функцией runAppM. Я попытался написать ConstraintKind, который выглядит как type (HasApp p m) = (RequiredPermission p ps ~ True, m ps), но это не сработало из-за Not in scope: type variable ‘ps’ - person Saurabh Nanda; 30.06.2019
comment
@K.A.Buhr Я пытался включить AllowAmbiguousTypes, но кажется, что компилятор все еще путается на месте вызова requiredPermission - gist.github.com/saurabhnanda/ - person Saurabh Nanda; 30.06.2019
comment
@K.A.Buhr Я пытался повозиться с вашим кодом, и проблема всплывает и там. Кажется, что нельзя использовать deleteA/readB изолированно. Они должны использоваться в сочетании с grantA/grantB для проверки типа кода. - person Saurabh Nanda; 30.06.2019
comment
Небольшое примечание: для удобства вы можете сделать type family Allowed' (p :: Permission) (ps :: [Permission]) :: Bool, а затем псевдоним типа type Allowed p ps = Allowed' p ps ~ 'True с ConstraintKinds, тогда вам не нужен ~ 'True на сайтах использования - person Jon Purdy; 30.06.2019
comment
Я думаю, что многие из них нужно было бы задать как дополнительные вопросы SO (и у вас, вероятно, есть полдюжины вопросов размера SO, которые вы задали здесь). Однако смотрите и мой другой ответ. - person K. A. Buhr; 30.06.2019