5

为了在外部保存用户帐户权限(例如在 DB 中),我想将具有派生Enum实例的枚举元素列表表示为Int.
数字的每一位都被视为一个标志(或布尔值),表示第 i 个元素是否存在于列表中。
用不同的话来说——2的每个幂代表一个元素,这些幂的总和是一个唯一的元素列表。

例子:

data Permissions = IsAllowedToLogin   -- 1
                 | IsModerator        -- 2
                 | IsAdmin            -- 4
                 deriving (Bounded, Enum, Eq, Show) 

enumsToInt [IsAllowedToLogin, IsAdmin] == 1 + 4 == 5

intToEnums 3 == intToEnums (1 + 2) == [IsAllowedToLogin, IsModerator]

将这样的列表转换为 an 的函数Int很容易编写:

enumsToInt :: (Enum a, Eq a) => [a] -> Int
enumsToInt = foldr (\p acc -> acc + 2 ^ fromEnum p) 0 . nub

请注意,接受的答案包含更有效的实现。

真正困扰我的是倒车功能。我可以想象它应该有这种类型:

intToEnums :: (Bounded a, Enum a) => Int -> [a]
intToEnums = undefined               -- What I'm asking about

我应该如何解决这个问题?

4

3 回答 3

10

以下是一个完整的解决方案。它应该表现得更好,因为它的实现是基于位而不是算术运算,这是一种更有效的方法。该解决方案还尽力概括事物。

{-# LANGUAGE DefaultSignatures #-}
import Data.Bits
import Control.Monad

data Permission = IsAllowedToLogin   -- 1
                | IsModerator        -- 2
                | IsAdmin            -- 4
                deriving (Bounded, Enum, Eq, Show) 

class ToBitMask a where 
  toBitMask :: a -> Int
  -- | Using a DefaultSignatures extension to declare a default signature with
  -- an `Enum` constraint without affecting the constraints of the class itself.
  default toBitMask :: Enum a => a -> Int
  toBitMask = shiftL 1 . fromEnum

instance ToBitMask Permission

instance ( ToBitMask a ) => ToBitMask [a] where 
  toBitMask = foldr (.|.) 0 . map toBitMask

-- | Not making this a typeclass, since it already generalizes over all 
-- imaginable instances with help of `MonadPlus`.
fromBitMask :: 
  ( MonadPlus m, Enum a, Bounded a, ToBitMask a ) => 
    Int -> m a
fromBitMask bm = msum $ map asInBM $ enumFrom minBound where 
  asInBM a = if isInBitMask bm a then return a else mzero

isInBitMask :: ( ToBitMask a ) => Int -> a -> Bool
isInBitMask bm a = let aBM = toBitMask a in aBM == aBM .&. bm

使用以下命令运行它

main = do
  print (fromBitMask 0 :: [Permission])
  print (fromBitMask 1 :: [Permission])
  print (fromBitMask 2 :: [Permission])
  print (fromBitMask 3 :: [Permission])
  print (fromBitMask 4 :: [Permission])
  print (fromBitMask 5 :: [Permission])
  print (fromBitMask 6 :: [Permission])
  print (fromBitMask 7 :: [Permission])

  print (fromBitMask 0 :: Maybe Permission)
  print (fromBitMask 1 :: Maybe Permission)
  print (fromBitMask 2 :: Maybe Permission)
  print (fromBitMask 4 :: Maybe Permission)

输出

[]
[IsAllowedToLogin]
[IsModerator]
[IsAllowedToLogin,IsModerator]
[IsAdmin]
[IsAllowedToLogin,IsAdmin]
[IsModerator,IsAdmin]
[IsAllowedToLogin,IsModerator,IsAdmin]
Nothing
Just IsAllowedToLogin
Just IsModerator
Just IsAdmin
于 2013-04-09T19:50:23.537 回答
4

我敢肯定 hackage 上已经有一些东西可以做到这一点,但是使用模块手动滚动你自己Data.Bits很简单。

您可以简化enumsToInt为类似的东西foldl' (.|.) . map (bit . fromEnum),即转换为整数索引,然后转换为单个位,然后使用按位或折叠。如果不出意外,这使您不必担心删除重复项。

因为intToEnums没有什么令人难以置信的方便,但为了快速解决方案,您可以执行类似filter (testBit foo . fromEnum) [minBound .. maxBound]. 这当然只适用于Bounded类型,并假定枚举的值不超过外部类型的位,并且fromEnum使用从 0 开始的连续整数,但听起来你是从所有这些作为前提开始的。

于 2013-04-09T19:49:17.687 回答
2

EnumSet可能正是您想要的。它甚至有一个intToEnums功能(尽管它似乎只与T Integer a我尝试过的类型一致 - 特别是T Int Char会给出意想不到的结果)并且在序列化/反序列化后不会重新创建重复条目(鉴于它是一个集合),而一份清单可能带有这种期望。

于 2013-04-09T19:43:43.403 回答