在类型级别编码身份验证的存在/不存在

时间:2016-02-06 20:59:55

标签: haskell yesod

上下文:从将运行时错误转换为编译时错误的角度来看,我正在接近Haskell。我的假设是,如果可以在程序类型本身内编写业务逻辑,那么这是可能的。

我正在写一个Telegram bot,我公司的用户应该可以访问它。要实现此限制",只要有人开始与机器人聊天,它就会在表格中查找chat_id并检查是否存在有效的oauth_token。如果没有,系统会首先向用户发送完成Google OAuth的链接(我们公司的电子邮件托管在Google Apps for Business上)。

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
VLUser
  email String
  chatId Integer
  tgramUserId Integer
  tgramFirstName String
  tgramLastName String Maybe
  tgramUsername String Maybe
  oauthToken String Maybe
  deriving Show
|]

具有有效oauth_token的用户将能够为Telegram bot提供一些命令,未经身份验证的用户无法提供这些命令。

现在,我试图在类型级别编写这个逻辑。在我的Haskell代码中将有一些函数能够接受作为参数的两者,经过验证和验证。未经认证的用户;而某些功能应该只接受经过身份验证的用户。

如果我继续传递相同类型的用户对象,即VLUser无处不在,那么我必须小心检查每个函数中是否存在oauthToken。有没有办法创建两种用户类型 - VLUserVLUserAuthenticated,其中:

  1. 两者都映射到相同的基础表
  2. VLUserAuthenticated只有oauthToken
  3. 才能实例化

1 个答案:

答案 0 :(得分:8)

Phantom types to the rescue!是Bryan O' Sullivan使用phantom types实现类型级别的读/写访问的示例。

同样,对于您的用例:

data Unknown       -- unknown users
data Authenticated -- verified users

newtype User a i = Id i deriving Show

重要数据构造函数Id 向用户公开,但该模块提供了初始化和验证用户的功能:

-- initializes an un-authenticated user
newUser :: i -> User Unknown i
newUser = Id

-- authenticates a user
authUser :: (User a i) -> User Authenticated i
authUser (Id i) = Id i  -- dummy implementation

然后,您可以控制类型级的访问权限,无需代码重复没有运行时检查而无需运行时成本

-- open to all users
getId :: User a i -> i
getId (Id i) = i

-- only authenticated users can pass through
getId' :: User Authenticated i -> i
getId' (Id i) = i

例如,如果

\> let jim = newUser "jim"
\> let joe = authUser $ newUser "joe"

joe是经过身份验证的用户,可以传递给任一函数:

\> getId joe
"joe"
\> getId' joe
"joe"

然而,如果您使用getId'致电jim,则会收到编译时错误

\> getId jim
"jim"
\> getId' jim   -- compile-time error! not run-time error!

<interactive>:28:8:
    Couldn't match type ‘Unknown’ with ‘Authenticated’
    Expected type: User Authenticated [Char]
      Actual type: User Unknown [Char]
    In the first argument of ‘getId'’, namely ‘jim’
    In the expression: getId' jim