2013-04-09 48 views
5

为了在外部(例如在数据库中)保存用户帐户权限,我想要表示一个枚举元素的列表,其中包含派生的Enum实例作为Int
数字的每一位都被看作是表示第i个元素是否出现在列表中的标志(或布尔值)。
换句话说 - 每个2的幂代表一个元素和这些权力的总和独特的元素列表。将Enums按列表形式列为Int

例子:

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] 

功能转换这样的列表为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 

我应该如何处理这个问题?

+2

对于初学者运行它,你看着[了'Data.Bits'模块(HTTP://hackage.haskell 。组织/包/归档/基/最新/ DOC/HTML /数据Bits.html)? – 2013-04-09 19:09:34

+0

@C。 A. McCann不,我没有!你认为这会有用吗? – Jakub 2013-04-09 19:13:32

+0

我不认为它有任何你想要的东西(虽然它看起来应该是那里的东西),但它有一堆按位操作,这会让事情变得更容易。 – 2013-04-09 19:15:25

回答

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 
+0

非常感谢!是否可以向类中添加默认定义,以便我可以简单地编写'实例ToBitMask权限'?当然假设'Permission'权限已经是'Enum'和'Bounded'的实例。 – Jakub 2013-04-09 20:38:24

+0

@Jakub请参阅更新。 – 2013-04-09 21:07:28

+0

@Jakub制作更新以进一步推广它,以防万一您感兴趣。 – 2013-04-10 14:59:50

2

EnumSet可能正是你想要的。它甚至有一个intToEnums函数(虽然它似乎只与我尝试过的类型T Integer a一致 - 特别是,T Int Char给出了意想不到的结果),并且在序列化/反序列化之后不会重新创建重复的条目(假设它有一组),而列表可能带有这种期望。

+0

是的,你需要一个更大的'Int'。除非您的机器使用128位字,否则您甚至不会将基本ASCII字符放入该EnumSet中。如果你想使用'Int',可以尝试'Ordering'或类似'(Bool,Bool)'的东西。 – 2013-04-09 19:59:07

4

我敢肯定,这件事已经有了,但它已经足够简单了,可以使用the Data.Bits module来自己动手。

您可以将enumsToInt简化为foldl' (.|.) . map (bit . fromEnum)之类的东西,即转换为整数索引,然后转换为单个位,然后使用按位或进行折叠。如果没有别的,这可以避免担心删除重复项。

对于intToEnums没有什么不可思议的方便,但对于快速解决方案,你可以做一些像filter (testBit foo . fromEnum) [minBound .. maxBound]。这当然只适用于Bounded类型,并且假定枚举没有比外部类型有位更多的值,并且fromEnum使用从0开始的连续整数,但是这听起来像是以所有这些作为前提无论如何。