使用提升的数据构造函数作为幻像参数

Using a promoted data constructor as a phantom parameter

在 Maguire 的 Thinking with Types 中,p. 29,有一个如何使用提升的数据构造函数作为幻像参数的示例。这是我根据书中的例子写的一个模块。

{-# LANGUAGE DataKinds #-}
module Main where

import           Data.Maybe
import           Data.Proxy

-- | The only purpose of this constructor is to give us access to its
--   promoted data constructors.
data UserType = DummyUser | Admin

-- | Give some users an administration token.
data User = User
  { userAdminToken :: Maybe (Proxy 'Admin),
    name           :: String
  }

doSensitiveThings :: Proxy 'Admin -> IO ()
doSensitiveThings _ = putStrLn "you did something sensitive"

trustedUser :: User
trustedUser = User (Just (Proxy :: Proxy Admin)) "Trust me"

main = do
    doSensitiveThings (fromJust . userAdminToken $ trustedUser)

我知道这使得在没有管理令牌的情况下无法调用 doSensitiveThings但我觉得我遗漏了一些重要的东西。 上面的代码比下面的代码好在哪里?

module Main where

import           Data.Maybe

data Admin = Admin

data User a = User String

doSensitiveThings :: User Admin -> IO ()
doSensitiveThings _ = putStrLn "you did something sensitive"

trustedUser :: User Admin
trustedUser = User "Trust me"

untrustedUser :: User ()
untrustedUser = User "Don't trust me"

main = do
    doSensitiveThings trustedUser
    -- doSensitiveThings untrustedUser -- won't compile

用原代码可以这样写:

trustedUser = User (Just Proxy) "trusted"
untrustedUser = User Nothing "untrusted"

twoUsers :: [User] -- or Map Username User or whatever
twoUsers = [trustedUser, untrustedUser]

您不能使用第二个代码片段制作类似的 twoUsers 列表,因为您的受信任和不受信任的用户具有不同的类型。

好吧,现在没有“a User”这样的东西了。 User AdminUser () 现在有不同的类型,所以你不能像对待它们一样对待它们。列表元素:

users :: [User] -- ill-kinded!
users = [User "untrusted" :: User (), User "trusted" :: User Admin] -- ill-typed!

您也不能再根据用户是否是管理员进行分支(请记住 Haskell 类型擦除!):

displayActions :: User a -> [String]
displayActions (User name) =
    ["Delete My Account (" ++ name ++ ")"] ++ (if isAdmin u then ["Delete Someone Else's Account"] else [])
isAdmin :: User a -> Bool -- this function can take either User Admin or User ()...
isAdmin = ??? -- ...but how's it supposed to branch on that?

也许试试

data SomeUser = SomeAdmin (User Admin) | SomeNormalUser (User ())

但现在我们基本上在您的第一个示例中做同样的事情(其中 User Admin 成为令牌类型而不是 Proxy Admin),只是更糟。只有很多代码噪音。

name :: SomeUser -> String -- having to write your own accessor functions over pattern matching/record fields; ew
name (SomeAdmin (User x)) = x
name (SomeNormalUser (User x)) = x -- ugly pattern matching and same code twice; ew
isAdmin :: SomeUser -> Bool
isAdmin (SomeAdmin _) = True
isAdmin _ = False

displayActions :: SomeUser -> [String] -- having both SomeUser and User instead of just one type and having to know which one to use in any given situation; ew
displayActions u =
    ["Delete My Account (" ++ name u ++ ")"] ++ (if isAdmin u then ["Delete Someone Else's Account"] else [])

我确实发现原版有问题,我相信这就是让您感到困惑的原因。原始代码中“唯一”的“好事”是令牌类型的存在。使用带有类型参数的 Proxy 来构造标记类型而不是

data AdminToken = AdminToken

(IMO)毫无意义且令人困惑(无论是对于理解技术还是在结果代码中)。类型参数 无关 与什么使这个想法好,并且通过保留类型参数而不是令牌,您将一无所获。我认为以下内容是对原作的 实际 改进,同时保持其好主意。

data User = { userAdminToken :: Maybe AdminToken; userName :: String }
isAdmin :: User -> Bool
isAdmin = isJust . userAdminToken

displayActions :: User -> [String]
displayActions u
    ["Delete My Account (" ++ userName u ++ ")"] ++ (if isAdmin u then ["Delete Someone Else's Account"] else [])