将枚举列表按位表示为Int类型



为了在外部保存用户帐户权限(例如在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]

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

我该如何处理这个问题?

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

{-# 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

我相信已经有一些东西在hackage上这样做了,但它很简单,可以使用Data.Bits模块手工卷自己的。

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

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

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

最新更新